From 35b539979844fb33a0592e55d22a50a89e090ea6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 5 Aug 2026 21:47:44 +0200 Subject: [PATCH 001/146] Extract dependencies --- .../app/aaps/core/data/pump/defs/DoseStepSize.kt | 12 ++++++++---- .../objects/extensions/TemporaryBasalExtension.kt | 11 +++++++++++ .../aaps/ui/compose/treatments/TempBasalScreen.kt | 3 ++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt index e0078146a641..8c3a6bc97ad8 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt @@ -1,6 +1,7 @@ package app.aaps.core.data.pump.defs -import java.util.Locale +import app.aaps.core.data.format.NumberFormat +import app.aaps.core.data.format.NumberFormatPlatform enum class DoseStepSize(private val entries: Array) { @@ -55,15 +56,18 @@ enum class DoseStepSize(private val entries: Array) { for (entry in entries) { if (first) first = false else sb.append(", ") - sb.append(String.format(Locale.ENGLISH, "%.3f", entry.value)) + sb.append(entry.value.dotted()) .append(" {") - .append(String.format(Locale.ENGLISH, "%.3f", entry.from)) + .append(entry.from.dotted()) .append("-") if (entry.to == Double.MAX_VALUE) sb.append("~}") - else sb.append(String.format(Locale.ENGLISH, "%.3f", entry.to)).append("}") + else sb.append(entry.to.dotted()).append("}") } }.toString() + /** Three decimals with a dot, whatever the device locale is. This text goes into logs. */ + private fun Double.dotted(): String = NumberFormat.DECIMAL_3.format(this, NumberFormatPlatform.SEPARATOR_DOT) + // to = this value is not included, but would actually mean <, so for rates between 0.025-0.975 u/h, we would have [from=0, to=10] internal class DoseStepSizeEntry(var from: Double, var to: Double, var value: Double) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt index b6d9fd951add..45b5200f4d25 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt @@ -18,6 +18,17 @@ import kotlin.math.min import kotlin.math.round import kotlin.math.roundToInt +/** + * True while the temporary basal is running. + * + * An extension taking [DateUtil], not a property on [TB], for the same reason as + * [app.aaps.core.objects.extensions.isInProgress] on `EB`: the answer depends on the current time, + * so the model itself stays a plain value that does not read the clock, and a test can control + * "now" instead of waiting for it. + */ +fun TB.isInProgress(dateUtil: DateUtil): Boolean = + dateUtil.now() in timestamp..timestamp + duration + fun TB.getPassedDurationToTimeInMinutes(time: Long): Int = ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt index 8e7cc0f1d3fa..0e95efe7716e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt @@ -42,6 +42,7 @@ import app.aaps.core.interfaces.aps.IobTotal import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.objects.extensions.iobCalc +import app.aaps.core.objects.extensions.isInProgress import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil @@ -127,7 +128,7 @@ fun TempBasalScreen( itemContent = { tb -> TempBasalItem( tempBasal = tb, - isActive = tb.isInProgress, + isActive = tb.isInProgress(viewModel.dateUtil), isFuture = tb.timestamp > viewModel.dateUtil.now(), isRemovingMode = uiState.isRemovingMode, isSelected = tb in uiState.selectedItems, From 67ecb1e6966bb1392317dd199c8502a616a8c651 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 5 Aug 2026 22:04:35 +0200 Subject: [PATCH 002/146] Going KMP --- _docs/KMP_IOS_FEASIBILITY.md | 634 ++++++++++++++++++ core/data/build.gradle.kts | 50 +- .../app/aaps/core/data/aps/AverageTDD.kt | 0 .../app/aaps/core/data/aps/BasalData.kt | 0 .../app/aaps/core/data/aps/SMBDefaults.kt | 0 .../aaps/core/data/configuration/Constants.kt | 0 .../app/aaps/core/data/format/NumberFormat.kt | 0 .../core/data/format/NumberFormatPlatform.kt | 27 + .../kotlin/app/aaps/core/data/iob/CobInfo.kt | 0 .../core/data/iob/InMemoryGlucoseValue.kt | 0 .../kotlin/app/aaps/core/data/iob/Iob.kt | 0 .../aaps/core/data/model/ActiveSceneState.kt | 0 .../kotlin/app/aaps/core/data/model/BCR.kt | 4 +- .../kotlin/app/aaps/core/data/model/BS.kt | 4 +- .../aaps/core/data/model/BolusWizardData.kt | 0 .../kotlin/app/aaps/core/data/model/CA.kt | 4 +- .../kotlin/app/aaps/core/data/model/CAL.kt | 4 +- .../kotlin/app/aaps/core/data/model/DS.kt | 4 +- .../kotlin/app/aaps/core/data/model/EB.kt | 4 +- .../kotlin/app/aaps/core/data/model/EPS.kt | 4 +- .../kotlin/app/aaps/core/data/model/FD.kt | 0 .../kotlin/app/aaps/core/data/model/GV.kt | 4 +- .../app/aaps/core/data/model/GlucoseUnit.kt | 0 .../kotlin/app/aaps/core/data/model/HR.kt | 4 +- .../kotlin/app/aaps/core/data/model/HasIDs.kt | 0 .../kotlin/app/aaps/core/data/model/ICfg.kt | 0 .../kotlin/app/aaps/core/data/model/IDs.kt | 0 .../kotlin/app/aaps/core/data/model/NE.kt | 0 .../kotlin/app/aaps/core/data/model/PS.kt | 4 +- .../kotlin/app/aaps/core/data/model/RM.kt | 4 +- .../kotlin/app/aaps/core/data/model/SC.kt | 4 +- .../kotlin/app/aaps/core/data/model/Scene.kt | 0 .../app/aaps/core/data/model/SceneAction.kt | 0 .../aaps/core/data/model/SceneEndAction.kt | 0 .../aaps/core/data/model/SceneLifecycle.kt | 0 .../app/aaps/core/data/model/SourceSensor.kt | 0 .../core/data/model/SourceSensorExtensions.kt | 0 .../kotlin/app/aaps/core/data/model/TB.kt | 7 +- .../kotlin/app/aaps/core/data/model/TDD.kt | 4 +- .../kotlin/app/aaps/core/data/model/TE.kt | 4 +- .../kotlin/app/aaps/core/data/model/TT.kt | 4 +- .../app/aaps/core/data/model/TTPreset.kt | 0 .../app/aaps/core/data/model/TimeStamped.kt | 0 .../app/aaps/core/data/model/TrendArrow.kt | 0 .../kotlin/app/aaps/core/data/model/UE.kt | 4 +- .../app/aaps/core/data/model/data/Block.kt | 0 .../aaps/core/data/model/data/TargetBlock.kt | 0 .../app/aaps/core/data/plugin/PluginType.kt | 0 .../aaps/core/data/pump/defs/Capability.kt | 0 .../aaps/core/data/pump/defs/DoseSettings.kt | 0 .../aaps/core/data/pump/defs/DoseStepSize.kt | 0 .../core/data/pump/defs/ManufacturerType.kt | 0 .../core/data/pump/defs/PumpCapability.kt | 0 .../core/data/pump/defs/PumpDescription.kt | 0 .../core/data/pump/defs/PumpTempBasalType.kt | 0 .../app/aaps/core/data/pump/defs/PumpType.kt | 0 .../core/data/pump/defs/TimeChangeType.kt | 0 .../app/aaps/core/data/time/SystemTimeZone.kt | 13 + .../kotlin/app/aaps/core/data/time/T.kt | 0 .../kotlin/app/aaps/core/data/ue/Action.kt | 0 .../kotlin/app/aaps/core/data/ue/Sources.kt | 0 .../app/aaps/core/data/ue/ValueWithUnit.kt | 0 .../app/aaps/core/data/ui/ConfirmationLine.kt | 0 .../data/format/NumberFormatPlatform.jvm.kt} | 26 +- .../aaps/core/data/time/SystemTimeZone.jvm.kt | 7 + .../aaps/core/data/format/NumberFormatTest.kt | 0 .../app/aaps/core/data/model/ICfgTest.kt | 0 .../data/model/SourceSensorExtensionsTest.kt | 0 .../data/format/NumberFormatPlatform.mingw.kt | 53 ++ .../core/data/time/SystemTimeZone.mingw.kt | 8 + 70 files changed, 822 insertions(+), 67 deletions(-) create mode 100644 _docs/KMP_IOS_FEASIBILITY.md rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/aps/AverageTDD.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/aps/BasalData.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/aps/SMBDefaults.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/configuration/Constants.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/format/NumberFormat.kt (100%) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/iob/CobInfo.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/iob/InMemoryGlucoseValue.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/iob/Iob.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/ActiveSceneState.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/BCR.kt (96%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/BS.kt (92%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/BolusWizardData.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/CA.kt (92%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/CAL.kt (86%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/DS.kt (79%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/EB.kt (91%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/EPS.kt (95%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/FD.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/GV.kt (81%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/GlucoseUnit.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/HR.kt (89%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/HasIDs.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/ICfg.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/IDs.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/NE.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/PS.kt (95%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/RM.kt (96%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SC.kt (92%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/Scene.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SceneAction.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SceneEndAction.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SceneLifecycle.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SourceSensor.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TB.kt (86%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TDD.kt (86%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TE.kt (98%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TT.kt (93%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TTPreset.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TimeStamped.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/TrendArrow.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/UE.kt (76%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/data/Block.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/model/data/TargetBlock.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/plugin/PluginType.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/Capability.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/DoseSettings.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/PumpTempBasalType.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/PumpType.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/pump/defs/TimeChangeType.kt (100%) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/time/SystemTimeZone.kt rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/time/T.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/ue/Action.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/ue/Sources.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/ue/ValueWithUnit.kt (100%) rename core/data/src/{main => commonMain}/kotlin/app/aaps/core/data/ui/ConfirmationLine.kt (100%) rename core/data/src/{main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt => jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt} (67%) create mode 100644 core/data/src/jvmMain/kotlin/app/aaps/core/data/time/SystemTimeZone.jvm.kt rename core/data/src/{test => jvmTest}/kotlin/app/aaps/core/data/format/NumberFormatTest.kt (100%) rename core/data/src/{test => jvmTest}/kotlin/app/aaps/core/data/model/ICfgTest.kt (100%) rename core/data/src/{test => jvmTest}/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt (100%) create mode 100644 core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt create mode 100644 core/data/src/mingwX64Main/kotlin/app/aaps/core/data/time/SystemTimeZone.mingw.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md new file mode 100644 index 000000000000..c8a9f6d72e6c --- /dev/null +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -0,0 +1,634 @@ +# Kotlin Multiplatform and a possible iOS client + +Written 2026-08-05. Notes from a review of how far AAPS is from sharing code with an iOS app. + +The goal that started this: **an iOS AAPSClient (follower), not a master app.** Not necessarily a +full KMP app, just a way to avoid writing everything again from zero. + +All numbers below were measured on the `dev` branch. They are counts of files under `src/`, +build folders excluded. + +--- + +## 1. Short answer + +There is **no KMP setup in the project today**. No module uses the multiplatform plugin. +(`:pump:combov2:comboctl` has `commonMain` / `androidMain` folder names left over from the upstream +project, but it builds as a normal Android library.) + +The shape of the code is much better than in a typical Android app, mostly because of the Compose +migration and the RxJava removal. The realistic path is **Kotlin Multiplatform with Compose +Multiplatform for the UI**, done step by step, starting with a small working slice on a real iPhone. + +--- + +## 2. What is already fine + +| Area | State | Why it matters | +|--------------------------------|------------------------------------------------------------------------------------|------------------------------------| +| `:core:data` | 58 files, plain `java-library`, **0** Android imports. Two `expect`/`actual` away, see section 8 | The first module to make multiplatform | +| `:core:keys` | 46 files, **0** Android and **0** `java.*` imports since Wave 3 | Only the 381 `R.string` ids block it | +| Room | 46 DAOs, **0** RxJava return types, 22 `suspend`, already on `BundledSQLiteDriver` | This is exactly the Room KMP setup | +| Compose | **0** XML layouts in `:core:ui`, 2 left in `:ui` | Compose Multiplatform can use this | +| `LocalContext.current` | 7 in `:core:ui`, 8 in `:ui` | Very small coupling to Android | +| Network code | 6 files touch Retrofit / OkHttp / socket.io | Small enough to rewrite on Ktor | +| kotlinx.serialization | 64 files (Gson: 34) | Already the main choice | +| kotlinx-datetime | Declared in `libs.versions.toml` | Ready to use | +| `androidx.lifecycle` ViewModel | Multiplatform since 2.8 | Most of the 129 uses are fine | +| Vico charts | Ships a `multiplatform` artifact | Only the artifact name changes | + +The biggest surprise was `:core:ui`. Of its 432 files: + +- 424 are under `compose/` +- **0** import Dagger or `javax.inject` +- 27 import `app.aaps.core.interfaces` +- 34 import `app.aaps.core.keys` +- **361 (84%) import none of Android, Dagger or the AAPS interfaces** + +So most of the Compose work of the last year can be reused. + +--- + +## 3. What does not work on iOS + +### Libraries with no Kotlin/Native version + +| Library | Files | Replacement | +|--------------------------------------------------------|--------------------------------------------------------|----------------------------------------| +| Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | +| RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | +| Retrofit + OkHttp | 6 | Ktor client | +| socket.io-client | 1 (`NSClientV3Service`) | See warning below | +| Gson | 34 | kotlinx.serialization | +| WorkManager | 44 | See warning below | +| joda-time | 5 | kotlinx-datetime | +| `java.text.DecimalFormat` | 74 | Done, see section 8 | +| `java.text.SimpleDateFormat` | 7 | kotlinx-datetime formatting | +| `java.util.concurrent.TimeUnit` | 108 files, but only 93 sites convertible | `kotlin.time.Duration` | +| `Executors`, `ConcurrentHashMap` | 14 | Coroutine dispatchers, map plus mutex | +| `java.security`, `javax.crypto` | ~20 | cryptography-kotlin, or expect/actual | +| `java.io.File` | 12 | okio | +| spongycastle, tink-android | 2 | as above | +| commons-lang3, Guava | 4 | Inline the few helpers | +| slf4j, logback-android | 4 | Kermit or Napier | +| kotlin-reflect | 9 | No reflection on Native, must go | +| Firebase | 4 | GitLive Firebase KMP, or expect/actual | +| Play Services | 2 | Android only by nature | +| androidx.glance (widgets) | 7 | WidgetKit is Swift only, no reuse | +| Garmin, osmdroid, androidsvg, appauth, java-otp, zxing | 1-3 each | Mostly not client features | + +### Android framework + +`Context` (28 impl, 28 sync, 16 source, **15 in `:core:interfaces`**), SharedPreferences, +notifications, the NSClient foreground service, DocumentFile / SAF (9 files), +Fragment / AppCompatActivity (12), and `R.string` (~136 `:ui`, 61 `:core:ui`, 60 impl, 60 sync). + +### Two that are not only porting cost + +- **socket.io** - Ktor gives WebSockets but not the Socket.IO / Engine.IO protocol on top. Either + reimplement the handshake, or use polling. +- **WorkManager** - iOS has no equivalent. `BGTaskScheduler` only gives wake ups that the system may + delay for hours. A follower that expects to run every 5 minutes cannot work that way. The real + answer is push (APNs) from a server, which changes the design, not only the code. + +### Habits that block portability + +1. **Android types in `:core:interfaces`** - 49 of 253 files import Android (`SP`, + `ResourceHelper`, `UiInteraction`, `PluginDescription`). It is the contract module, so anything + depending on it inherits Android. +2. **Resource ids used as data** - `@StringRes Int` inside `PluginDescription`, notifications and + `UserEntryPresentationHelper`. An `Int` resource id means nothing off Android. +3. **`PluginBase` is an Android class** - the plugin registry is the spine of the app. +4. **Field `@Inject` into Fragments and Services** - constructor injection ports, field injection + does not. +5. **RxBus as a global untyped bus** - the main reason logic classes cannot move. +6. **Reflection based serialization** - Gson plus kotlin-reflect. +7. **Assuming background execution is always possible** - the deepest assumption of all. + +--- + +## 4. Options + +### A - Native SwiftUI client, no shared code + +A follower reads Nightscout, shows it, and writes treatments back. That is a REST and socket +contract, not AAPS code. LoopFollow and Nightguard already do this. Cheapest, but nothing is +shared and the two apps drift apart forever. + +### B - Shared KMP core, SwiftUI on top + +Share `:core:data`, `:core:keys`, a Ktor Nightscout client, the Room layer, and the calculations +(IOB / COB, profile, TIR / TDD). UI written twice. + +### C - KMP plus Compose Multiplatform <- recommended + +Share logic **and** UI. This is more realistic here than in most projects because the UI is already +Compose and 84% of `:core:ui` has no Android or DI coupling. + +The first thought was that this drags the whole repo onto KMP. **That was wrong.** A +`kotlin("multiplatform")` module with `androidTarget()` publishes a normal Android variant, so +`:app`, `:plugins:*` and the pump drivers keep consuming it unchanged. The KMP plugin applies per +module, not per repo. + +The decisive argument for C is maintenance, not the initial cost: with two UIs, every future screen +has to be built twice, in two languages, forever. + +### Scope note + +A client does not need all ~50 modules. Roughly a dozen: `:core:data`, `:core:keys`, +`:core:interfaces` (narrowed), `:core:ui`, `:core:graph`, `:database:*`, the Nightscout sync path +and the overview UI. Pump drivers, automation, SMS, Garmin and wear never need to be KMP. + +### One honest warning + +Compose Multiplatform on iOS is stable, but it does not feel like a native iOS app. Scrolling, +text fields and accessibility differ from SwiftUI. For an app that people build themselves this is +probably an acceptable trade, but **test it on a real iPhone early**, not after months of work. + +--- + +## 5. Practical setup for iOS + +### Hard requirements + +1. **A Mac.** Xcode runs only on macOS. Needed for every option, even Compose Multiplatform, because + the app shell, signing and device runs all go through Xcode. +2. **Apple Developer Program, 99 USD per year.** A free Apple ID works but certificates expire after + **7 days**, there is no TestFlight, and **push notifications are not available**. Since push is + the only real answer to the iOS background limits, the paid account is in practice required. + +### Where the code should live + +``` +AndroidAPS repo (this one) + shared/client-core/ <- the only module with kotlin("multiplatform") + nothing else in the repo changes + | CI builds an XCFramework and publishes it (SPM) + v +AAPSClient-iOS repo (new) + AAPSClient.xcodeproj + Sources/ <- Swift, or a thin shell around Compose Multiplatform +``` + +Reasons: + +- The shared Kotlin **must** stay in this repo. It is taken from the existing code and would rot in + a separate repo within one release. +- The iOS app should **not** be here. An `iosApp/` folder would force the KMP toolchain on every + Android contributor and a Mac runner on the main CI. +- AndroidAPS is public, so **GitHub Actions macOS runners are free** for building the framework. + +### Tools + +| Task | Tool | +|----------------------------|------------------------------------------| +| Shared Kotlin | Android Studio or IntelliJ, as today | +| Swift and SwiftUI | Xcode (required) | +| Run on device or simulator | Xcode | +| Signing and provisioning | Xcode | +| Compose Multiplatform UI | Android Studio, Xcode only for the shell | + +### What to learn + +- **Swift** is close to Kotlin. Optionals, closures, `struct` / `class`, `protocol` as interface. + One to two weeks to be productive. +- **SwiftUI** is close to Compose. `VStack` is `Column`, `@State` is + `remember { mutableStateOf() }`, + `.padding()` is `Modifier.padding()`. +- **Xcode itself is the painful part**, not Swift. Signing, provisioning profiles and entitlements + are where the first days go. +- For KMP, use **SKIE** (Touchlab, free). Without it Kotlin `suspend` becomes completion handlers + and `Flow` does not bridge to Swift at all. + +### Distribution + +The App Store is not realistic for this kind of app. The working model already exists in this +community: **the Loop approach** - the user forks the repo, puts their own Apple credentials into +GitHub secrets, and GitHub Actions builds and uploads to **their own** TestFlight. Builds last +90 days and renew automatically. No Mac needed by the end user. + +This is real work, not an afterthought. Plan for it. + +--- + +## 6. Resources and translations + +**Translations survive almost unchanged.** Compose Multiplatform Resources uses the same +`strings.xml` format and the same `values-XX` folder names as Android. + +``` +core/ui/src/commonMain/composeResources/ + values/strings.xml <- English + values-cs-rCZ/strings.xml <- the same files as today + values-de-rDE/strings.xml + drawable/ font/ files/ +``` + +**Crowdin only needs a path change** in `crowdin.yml`, from +`src/main/res/values-%android_locale%/strings.xml` to +`src/commonMain/composeResources/values-%android_locale%/strings.xml`. Translation memory and the +existing translations are untouched. + +### Three real problems + +1. **`getString()` outside Compose is `suspend`.** In a Composable, `stringResource(Res.string.x)` + is normal. Everywhere else (ViewModels, workers, notifications, Nightscout upload) the function + is suspending. `ResourceHelper.gs()` is used from about 60 files in `:implementation` and 60 in + `:plugins:sync`, and those are not Composables. + Two answers, both worth using: + - Keep user visible strings **out of shared logic**. Return typed errors or enums and map them + to + text in the UI layer. Same idea as the existing rule about keeping `@ColorInt` out of domain + models. + - Where a string really must be resolved off the UI thread, `moko-resources` gives synchronous + access through `StringDesc`. +2. **iOS needs `CFBundleLocalizations` in `Info.plist`.** Without it iOS reports the wrong preferred + language and resource lookup quietly falls back to English. +3. **`Res` is generated per module**, so `:core:ui` and `:ui` each have their own. Same situation as + today with `app.aaps.core.ui.R` and `app.aaps.ui.R`. + +### What this means for `:core:keys` + +The 368 strings themselves move without trouble. The real work is that the key classes carry +`@StringRes Int`, and a Compose Multiplatform resource is a `StringResource`, not an `Int`. That +type change is the job, not the translations. + +--- + +## 7. Suggested order of work + +**Do not remove all blockers first and start KMP afterwards.** That is how large migrations die: +a year of work against a static list, fixing things that turn out not to matter, with no feedback. + +| Step | Work | +|-------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **0** | **Thin slice on a real iPhone first.** `:core:data` to KMP, a small Ktor call to Nightscout, one Compose Multiplatform screen showing a glucose value. Weeks, not months, because `:core:data` is already clean. | +| 1 | `DecimalFormat` and `TimeUnit` cleanup (see section 8) | +| 2 | `:core:keys` off resource ids - this gates everything in `:core:ui` | +| 3 | Narrow common interfaces out of `:core:interfaces` | +| 4 | `:core:ui` to KMP, resources to compose-resources | +| 5 | Ktor Nightscout client, Room KMP | +| 6 | Xcode shell, Koin or kotlin-inject, SKIE, first real screen | + +Step 0 proves the toolchain works, gives an honest answer about how Compose Multiplatform feels on +iOS, and makes every later step demand driven: a blocker is removed because it stands between you +and the next screen, not because it is on a list. + +--- + +## 8. Work done so far + +Committed on `dev`: + +| Commit | What | +| --- | --- | +| `e5f4e27626` | Migrate DecimalFormat | +| `a42d823c93` | Eliminate TimeUnit | +| `e1068e77db` | `:core:keys` remove JVM dependency | + +### Wave 1 - `DecimalFormat` removed + +Survey found **177 uses in 74 files**, 19 different patterns, and **29 places where +`java.text.DecimalFormat` appeared in an API signature** - including Compose parameters such as +`valueFormat: DecimalFormat = DecimalFormat("0.0")` in `SliderWithButtons`, `PlusMinusEdit`, +`ValueInputDialog`, `NumberInputRow` and five more in `:ui`. Those signatures were blocking +`:core:ui` from `commonMain`, so this step is on the critical path for Compose Multiplatform. + +**What was added** in `:core:data`: + +- `app.aaps.core.data.format.NumberFormat` - pure Kotlin. Holds `minIntegerDigits`, + `minFractionDigits`, `maxFractionDigits` plus named constants for all 17 patterns found + (`INTEGER`, `DECIMAL_1` to `DECIMAL_3`, `DECIMAL_6`, `UP_TO_2_DECIMALS`, + `DECIMAL_1_UP_TO_2`, ...). +- `NumberFormatPlatform` - the only remaining user of `java.text`. Becomes `expect` / `actual` when + `:core:data` goes KMP. + +**The formatting is not reimplemented.** Matching `DecimalFormat` half-even rounding on doubles bit +for bit in pure Kotlin is hard, and the test suite cannot detect a mistake (see the locale note +below). Delegating gives the KMP win with no change in behaviour. + +Two details that would have caused silent bugs: + +- `isGroupingUsed = false`. `DecimalFormat()` groups by default, so `1234.5` would have become + `1,234.5`. +- A `ThreadLocal` cache. `DecimalFormat` is not thread safe, and `StringUtil` currently shares one + mutable array between threads. + +**The cache key must hold the locale.** A first version keyed it on (format, separator) only. A +cached `DecimalFormat` carries the WHOLE symbol set, not only the separator, and the app can change +language while it runs (`ComposeMainActivity` calls `recreate()`, the process stays alive). Swedish, +Norwegian and Lithuanian use a comma AND a real MINUS SIGN (U+2212); German and Czech use a comma +and a plain hyphen. Same key, different symbols, so after a language switch negative numbers kept +the old sign until the app was killed. Fixed by putting `Locale` in the key. The test +`symbols follow the locale after it changes` switches de -> sv -> de and en -> ne-NP and checks both +the sign and the digit shapes. + +**Locale behaviour is unchanged on purpose.** The separator comes from the platform, so a Czech or +German device still shows a comma. `format(value, SEPARATOR_DOT)` is the explicit way to ask for a +dot, for server data and files. + +**Result:** 57 files across 11 modules migrated, all 29 API leaks closed, `:app` and `:wear` +compile, +**2319 unit tests pass with 0 failures**. + +**A latent bug was found and neutralised.** 13 automation triggers used `DecimalFormat("1")` and +`DecimalFormat("0.1")`. Those are not digit placeholders - the digits are literals: + +``` +"1" format(100.0) -> "1100" format(-5.5) -> "-16" +"0.1" format(720.0) -> "720.1" format(0.5) -> "0.1" +``` + +It never reached users, because `decimalFormat` in `InputDouble`, `InputDelta` and `InputBg` is a +private field that is only written, never read. This was checked properly afterwards, including the +JSON that triggers write to preferences and sync - see "Was behaviour preserved?" below. The dead +parameter is worth deleting separately. + +**Two files were left alone on purpose:** + +| File | Reason | +|-------------------------------------------|------------------------------------------------------------------| +| `wear/SmallestDoubleString.kt` | Builds patterns from a runtime string, needs real logic | +| `shared/impl/src/test/DateUtilOldImpl.kt` | A frozen copy of the old implementation kept as a test reference | + +### Two locale bugs fixed on the way + +**`StringUtil.getFormattedValueUS` is now the extension `Number.formatUS(decimals)`.** It builds the +`DecimalFormat` with `DecimalFormatSymbols(Locale.US)` instead of formatting with the device locale +and then replacing a comma with a dot. That replace did nothing on locales that use another +separator, for example Arabic. A new test checks the output in `en`, `de-DE`, `cs-CZ`, `fr-FR` and +`ar-SA`. + +It stays on `java.text` and **adds no module dependency**. `:core:utils` carries Firebase, +WorkManager, spongycastle and tink, so it is not a KMP candidate anyway, and a +`:core:utils -> :core:data` edge would only slow the build down. + +The old shared `DecimalFormatters` array is gone too. `DecimalFormat` is not thread safe, so two +threads formatting at the same time could produce wrong text. + +**`DateUtilImpl.qs()` no longer groups.** The old code used `DecimalFormat()` with no pattern, which +groups by default, and only the decimal separator was overridden. The grouping separator stayed +locale dependent, so for values over 1000: + +| locale | `qs(1234.5, 1)` before | after | +|--------|------------------------|----------| +| en | `1,234.5` | `1234.5` | +| de | `1.234.5` (two dots) | `1234.5` | +| cs | `1234.5` | `1234.5` | +| ar | Arabic-Indic digits | `1234.5` | + +Grouping was never intended - the method forces a dot separator, so it clearly wants neutral output. +Its only caller is `niceTimeScalar` ("3 days", "45 minutes"). No grouping support had to be added to +`NumberFormat`. + +### Wave 2 - duration types (done) + +**`java.util.concurrent.TimeUnit` really is a blocker** - it does not exist on Kotlin/Native. Of 226 +uses in 108 files, **94 were constant conversions and are now `kotlin.time`**: + +``` +TimeUnit.MINUTES.toMillis(x) -> x.minutes.inWholeMilliseconds +TimeUnit.MILLISECONDS.toMinutes(x) -> x.milliseconds.inWholeMinutes +``` + +51 files changed. The other 111 uses stay, because the API they are passed to demands a `TimeUnit`: +`Single.timeout` (36), `WorkRequest.setInitialDelay` (8), `Semaphore.tryAcquire` (7), RxJava +schedulers, OkHttp timeouts. Those files keep the import. + +Two traps that a plain search and replace would have fallen into: + +- `plugins/automation/elements/InputDuration.kt` declares its **own** `enum class TimeUnit`, and + `ActionStartTempTarget.kt` imports both it and `java.util.concurrent.TimeUnit`. The local one is + always written as `InputDuration.TimeUnit.MINUTES`, so matching on `TimeUnit..to(` + cannot hit it - but a rename over the bare word would have broken it. +- Three calls in `:pump:eopatch` nest one `TimeUnit` call inside another, for example + `TimeUnit.MILLISECONDS.toMinutes(millis - TimeUnit.HOURS.toMillis(hours))`. Those were done by + hand, since the argument needs balanced bracket matching, not a regex. + +**`T` (`core/data/time/T.kt`) was left as it is.** It is imported by 221 files, and it is **not** a +KMP blocker: it is pure Kotlin in a pure Kotlin module. `T.now()` was the only line using a JVM API +(`System.currentTimeMillis()`), and it turned out to have **no callers at all** apart from its own +test, so it was deleted. `T` is now fully portable as written, and nothing forces a migration. + +Replacing `T` with `kotlin.time.Duration` everywhere was tried and then reverted. It gives no +portability gain, and deprecating a class used by 221 files costs about that many build warnings for +everyone until the files move over. If it is ever done, keep the quirk that `T.months(n)` counts a +month as 31 days. + +### Wave 3 - `:core:keys` off the JVM (done) + +`ComposedKey.composeKey` used `String.format(Locale.ENGLISH, key + format, *arguments)`. It is now +plain Kotlin that walks the template itself. + +Two reasons, and the first matters more than the portability one: + +1. The result becomes a **SharedPreferences key name**. `String.format` follows a locale, and a + locale with its own digits (Nepali, Bengali, Burmese, ...) would write `%d` in that script. The + key would differ and the stored setting would be lost. `Locale.ENGLISH` was passed to avoid that; + building the text by hand removes the risk instead of working around it. +2. `String.format` is JVM only. + +Every format in the app is `%d` or `%s` (19 uses, nothing else), so no library and no `expect` / +`actual` is needed. Anything else now **throws** `IllegalArgumentException`: an unknown specifier, a +lonely `%`, a wrong argument count, or a `%d` given something that is not a whole number. That last +guard matters - `String.format("%d", 1.5)` used to throw, while plain substitution would have +quietly produced a different key and lost the setting. + +`ComposedKeyTest` compares the new output against the real `String.format(Locale.ENGLISH, ...)` for +**every composed key enum in the app**, iterating `.entries` so a new key is covered automatically, +and repeats it in `ar-SA`, `ne-NP`, `bn-IN`, `my-MM`, `fa-IR` and `th-TH-u-nu-thai`. + +`:core:keys` had no test source set, so `id("test-module-dependencies")` was added. Test only, no +effect on the main compile. + +**What is left in `:core:keys`: nothing but resources.** Zero `android.*` imports, zero `java.*` +imports. The only blocker is **381 `R.string` references in 6 files** - the keys carry preference +titles and summaries as raw `Int` resource ids. That is the resources job in section 6, not a +separate problem. + +### Wave 4 - `:core:data` ambient state (partly done) + +Two reads of ambient system state were removed from the models: + +- **`TB.isInProgress`** was a property computing `System.currentTimeMillis() in timestamp..end`. It + is now `fun TB.isInProgress(dateUtil: DateUtil)` in `:core:objects`, matching the `EB` sibling two + lines away in the same file, which already worked that way. One call site. Besides portability it + removes a hidden non-determinism: the same object could compare unequal to itself across a tick, + and it could not be tested without waiting for real time. +- **`DoseStepSize.description`** used `String.format(Locale.ENGLISH, "%.3f", ...)`; it now uses + `NumberFormat.DECIMAL_3` with `SEPARATOR_DOT`. The Wave 1 work had already built the replacement. + +**`java.util.TimeZone` in 17 model files stays, and should be solved with `expect` / `actual`.** + +Every one of them is the same line: + +```kotlin +var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), +``` + +Removing the default and passing `utcOffset` in from the callers was tried and **reverted**. Why it +is the wrong shape here: + +- `:core:data` cannot use `DateUtil`. `DateUtil` lives in `:core:interfaces`, which already depends + on `:core:data`, so the reverse edge is a cycle. And a data class default parameter has nowhere to + receive an injected dependency anyway. +- The ripple is large. The compiler first reported 2 errors; the real count was 38 and still growing + when the attempt was stopped, with test sources not yet compiled. Gradle stops at the first failing + module and Kotlin caps reported errors per file - in `PersistenceLayerImpl` it named 10 of 31. +- **`utcOffset` is not a throwaway field.** It is stored in every table, it is part of + `contentEqualsTo` (so a changed value makes a record compare unequal and re-sync), it is uploaded + to Nightscout - where the server validates it and `NSAndroidClientImpl` has to catch + `"Bad or missing utcOffset field"` 400s and retry with 0 - it goes to Open Humans, and it is shown + in the user entry history. Recomputing it by hand at 50+ sites risks silently changing stored + values; `expect` / `actual` keeps the computation identical at every site and changes no call site. + +So when `:core:data` becomes multiplatform: + +```kotlin +// commonMain +expect fun systemUtcOffsetAt(timestamp: Long): Long // the models keep their default +// jvmMain -> TimeZone.getDefault().getOffset(timestamp) +// iosMain -> NSTimeZone +``` + +The same reasoning does **not** apply to `isInProgress`. That is a value computed on demand at +arbitrary times, so passing `DateUtil` is right there. A constructor default evaluated once at +creation is the weaker case, and the 154 call sites that care already pass `utcOffset` explicitly. + +**State of `:core:data` now:** + +| Was | Now | +| --- | --- | +| `System.currentTimeMillis()` | gone (`T.now()` deleted, `isInProgress` moved out) | +| `java.util.Locale` | gone (`DoseStepSize`) | +| `java.util.concurrent.TimeUnit` | gone (Wave 2) | +| `java.util.TimeZone`, 17 files | `expect` / `actual`, no call site changes | +| `NumberFormatPlatform` | `expect` / `actual`, by design | + +Two `expect` / `actual` declarations away from compiling as `commonMain`. + +### Was behaviour preserved? + +An audit ran five parallel agents against the migrated code, each trying to find an input where old +and new differ, with a second pass trying to refute what they found. Results worth keeping: + +- **`DecimalFormat` -> `NumberFormat`: identical.** 17 patterns against ~90 values across 57 locales, + then all 17 against every `Locale.getAvailableLocales()` (~800). Zero differences, including NaN, + infinities, -0.0, 1e300 and the half-even boundaries. +- **`TimeUnit` -> `kotlin.time`: identical where it can be reached.** Checked by loading the real + `kotlin-stdlib-2.4.10.jar` and comparing by reflection over 42 million values. They diverge only + past about 146 million years, which no call site can reach. All 93 changed lines were checked + individually for operator precedence, including the `?:` re-parenthesising in `TreatmentMapper` + and the `hours.hours` local-shadowing in `PatchManager`. +- **The automation `DecimalFormat("1")` change really is invisible.** `:plugins:automation` has + neither Gson nor kotlinx.serialization, `Trigger.toJSON()` is hand written and emits only + value/comparator/units, and the reflection in `ActionEditors.kt` reads only `"text"`, `"daysBack"` + and friends. The field is written and never read. + +Three real changes, all of them fixes, and one bug that the audit caught and that is now fixed (the +formatter cache, above): + +| Change | Effect | +| --- | --- | +| `qs()` no longer groups | `1234.5` with 1 digit: en `1,234.5` -> `1234.5`, de `1.234.5` -> `1234.5` | +| `formatUS` uses `Locale.US` symbols | correct in Arabic; also swaps U+2212 for `-` on sv, nb, lt, hr, fi, sl, et | +| automation `%d` patterns | the literal-digit bug is gone, but nothing renders it | + +`qs()` also gained a `max(0, digits)` guard: `DecimalFormat` used to clamp a negative +`maximumFractionDigits` to 0, while `NumberFormat` throws. No caller passes a negative, but the guard +keeps the old contract on a public interface method. + +--- + +## 9. Open decisions + +1. ~~Add `:core:utils` -> `:core:data`?~~ **Decided: no.** It would slow the build down for no real + gain, and `:core:utils` is not a KMP candidate. `StringUtil` was fixed in place instead, with + `Locale.US` symbols and no new dependency. +2. ~~Add grouping support to `NumberFormat`?~~ **Decided: not needed.** The grouping in + `DateUtilImpl.qs()` was accidental and broken, so it was removed rather than reproduced. +3. ~~Wave 2 size?~~ **Decided: convert `TimeUnit` only, leave `T` alone.** `TimeUnit` is a real + blocker and had to go. `T` is not, so replacing it would have been style work paid for with about + 221 build warnings. The dead `T.now()` was removed instead, which is a real cleanup with no cost. +4. ~~Use `DateUtil` instead of `System.currentTimeMillis()` everywhere?~~ **Decided: only where it is + needed.** The principle is right - `DateUtil` is an interface, so it mocks in tests and can differ + per platform - but the sweep would be **547 sites in 227 files**, plus 62 `TimeZone.getDefault()` + in 57 files, and most of it sits in pump drivers that will never be KMP. Do it in the modules on + the KMP path, leave `:pump:*` and `:wear` alone, and make it a rule for new code so the number + stops growing. +5. **Still open: which branch for the first KMP module.** Converting `:core:data` to + `kotlin("multiplatform")` changes how Gradle resolves it for the 13 modules that depend on it. + That is the most valuable thing to find out and the most likely thing to break, so a throwaway + branch is safer than `dev`. +6. **Still open: `wear/SmallestDoubleString.kt`** - the last `DecimalFormat` in a non-test file that + is not the deliberate platform seam. It builds patterns from a runtime string, so it needs real + logic rather than a mapping. + +Waves 1 to 3 are committed. Still in the working tree: the `TB.isInProgress` extension and the +`DoseStepSize` change from Wave 4. The `FoodManagement` comma defect in section 10 is found but not +fixed. + +--- + +## 10. Side finding - the decimal separator + +While reviewing the `.replace(",", ".")` calls, the history turned out to be relevant. + +**On a comma locale the numeric keyboard gives a comma, and `toDouble()` accepts only a dot.** +The project has been bitten by this before: + +- `52830f620f` "Allow comma in NumberPicker" (2019) - AAPS forked the platform `DigitsKeyListener` + into a 199 line `DigitsKeyListenerWithComma`, because the stock one swallowed the comma key and + `"1,5"` became `"15"`. +- `5b36b312ce` "Correction bug Steampunk" (2019) - a watch face parsed a delta string the phone had + formatted with the device locale, and threw on comma locales. This is exactly the + format then reparse round trip. +- `f9fa82f1ef` "replace swedish minus sign" (2018) - the Swedish keyboard emits U+2212, so the parse + silently returned 0 instead of a negative number. + +**Today the app is well defended.** Only 4 places use `KeyboardType.Decimal`, and three of them +strip +the comma: `NumberInputRow` (4 read points), `PlusMinusEdit`, `ValueInputDialog`. `SafeParse` does +it +too. There is no `android:inputType` left anywhere - all text input is Compose now. + +### One live defect + +`FoodManagementScreen.kt:357` uses `KeyboardType.Decimal` with a raw `OutlinedTextField`, and +`FoodManagementViewModel.kt:173` parses it with: + +```kotlin +portion = state.editorPortion.toDoubleOrNull() ?: 0.0 +``` + +A user on a comma locale types `12,5`, and the food is saved with `portion = 0.0`. No error, no red +field, Save stays enabled. On an edit this destroys the old value, and the `0.0` syncs to +Nightscout. + +**Severity is low.** `portion` is display only - nothing multiplies by it, and the wizard receives +`food.carbs` (an `Int`, `KeyboardType.Number`). So this is lost metadata, not a wrong dose. The fix +is to use the shared `NumberInputRow` instead of a raw text field. + +### The tests cannot see locale bugs + +`TestBase.kt:28` calls `Locale.setDefault(Locale.ENGLISH)` with no restore, so it leaks into the +whole Gradle test worker. There is no `@Config(qualifiers = ...)` anywhere. On top of that, 19 test +assertions normalise the separator themselves: + +```kotlin +assertThat(sut.to1Decimal(1.33).replace(",", ".")).isEqualTo("1.3") // passes for "1,3" too +``` + +Those tests cannot fail on a locale bug. Worth noting that `7a5d8e2ec9` "use US locale in tests" +(2018) fixed this properly for `ProfileSealedTest` by pinning the locale, and the 2020 Kotlin +rewrite `89d1de9710` lost the pin and put the `.replace()` back. + +This is why the new `NumberFormatTest` sets `ENGLISH`, `GERMAN` and `cs-CZ` explicitly, compares raw +output against real `DecimalFormat` for 17 patterns and 43 values, and restores the locale +afterwards. + +### Not merged, maybe worth taking + +`b7b1571f72` "OTP: format token with `Locale.ROOT` so it stays ASCII digits" exists only on +`origin/fix/code-quality-audit`. On `dev`, `OneTimePassword.kt:75` still uses `Locale.getDefault()`, +so an Arabic locale device would render OTP tokens with Arabic-Indic digits. diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 36579954b059..bee18f014f99 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -1,19 +1,47 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") + kotlin("multiplatform") } -java { - sourceCompatibility = Versions.javaVersion - targetCompatibility = Versions.javaVersion -} +kotlin { + jvm { + // Every consumer of :core:data is still an Android or JVM module, so this variant is the + // one they resolve. Nothing about them changes. + compilerOptions { + jvmTarget.set(Versions.jvmTarget) + } + } + + // Stand-in for a real Kotlin/Native target. iOS targets need macOS and Xcode, which cannot run + // on Windows, but mingwX64 compiles the same common code through Kotlin/Native, so it proves + // there is no JVM API left in commonMain. Replace or extend with + // iosArm64() / iosSimulatorArm64() on a Mac. + mingwX64 { + compilerOptions { + // kotlin.assert has an experimental implementation on Native. ICfg.iobCalcForTreatment + // uses it. Opting in here keeps that code exactly as it is - turning the asserts into + // require() would change behaviour, because JVM assertions are off in production while + // require() always throws. + optIn.add("kotlin.experimental.ExperimentalNativeApi") + } + } -dependencies { - testImplementation(libs.org.junit.jupiter) - testImplementation(libs.com.google.truth) - testRuntimeOnly(libs.org.junit.platform.launcher) + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val jvmTest by getting { + dependencies { + implementation(libs.org.junit.jupiter) + implementation(libs.com.google.truth) + runtimeOnly(libs.org.junit.platform.launcher) + } + } + } } tasks.withType { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/core/data/src/main/kotlin/app/aaps/core/data/aps/AverageTDD.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/aps/AverageTDD.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/aps/AverageTDD.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/aps/AverageTDD.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/aps/BasalData.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/aps/BasalData.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/aps/BasalData.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/aps/BasalData.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/aps/SMBDefaults.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/aps/SMBDefaults.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/aps/SMBDefaults.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/aps/SMBDefaults.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/configuration/Constants.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/configuration/Constants.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/configuration/Constants.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/configuration/Constants.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/format/NumberFormat.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/format/NumberFormat.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt new file mode 100644 index 000000000000..69db6ab373e7 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt @@ -0,0 +1,27 @@ +package app.aaps.core.data.format + +/** + * Renders a [NumberFormat] as text. + * + * This is the only place in the app that still uses a platform number formatter. Everything else + * works with [NumberFormat], which is pure Kotlin. + * + * The locale data (separators, minus sign, digit shapes) belongs to the platform - it is the CLDR + * database that ships with the JVM and with iOS. Writing it again in common Kotlin would mean + * carrying megabytes of it and then disagreeing with the operating system, so this stays a seam. + * + * Output must match the old `java.text.DecimalFormat` patterns: no grouping separator, rounding + * half-even. `NumberFormatTest` in `jvmTest` checks that against the real `DecimalFormat`. + */ +expect object NumberFormatPlatform { + + /** Decimal separator for text that must not depend on the locale, for example server data. */ + val SEPARATOR_DOT: Char + + /** Decimal separator of the current locale. */ + val localeSeparator: Char + + fun format(format: NumberFormat, value: Double): String + + fun format(format: NumberFormat, value: Double, separator: Char): String +} diff --git a/core/data/src/main/kotlin/app/aaps/core/data/iob/CobInfo.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/iob/CobInfo.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/iob/CobInfo.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/iob/CobInfo.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/iob/InMemoryGlucoseValue.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/iob/InMemoryGlucoseValue.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/iob/InMemoryGlucoseValue.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/iob/InMemoryGlucoseValue.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/iob/Iob.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/iob/Iob.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/iob/Iob.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/iob/Iob.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/ActiveSceneState.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ActiveSceneState.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/ActiveSceneState.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/ActiveSceneState.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/BCR.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/BCR.kt similarity index 96% rename from core/data/src/main/kotlin/app/aaps/core/data/model/BCR.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/BCR.kt index df17cc9d09ae..58e47497c632 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/BCR.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/BCR.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class BCR( override var id: Long = 0, @@ -10,7 +10,7 @@ data class BCR( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var targetBGLow: Double, var targetBGHigh: Double, var isf: Double, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/BS.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/BS.kt similarity index 92% rename from core/data/src/main/kotlin/app/aaps/core/data/model/BS.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/BS.kt index 15c490b185cb..38ebd95fba5f 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/BS.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/BS.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class BS( override var id: Long = 0, @@ -10,7 +10,7 @@ data class BS( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var amount: Double, var type: Type, var notes: String? = null, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/BolusWizardData.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/BolusWizardData.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/BolusWizardData.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/BolusWizardData.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/CA.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/CA.kt similarity index 92% rename from core/data/src/main/kotlin/app/aaps/core/data/model/CA.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/CA.kt index 3c286fab03d9..93181686e755 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/CA.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/CA.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt import kotlin.math.abs import kotlin.time.Duration.Companion.hours @@ -12,7 +12,7 @@ data class CA( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), /** Duration in milliseconds */ var duration: Long, var amount: Double, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/CAL.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/CAL.kt similarity index 86% rename from core/data/src/main/kotlin/app/aaps/core/data/model/CAL.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/CAL.kt index 0fe7da81ecda..6ae6d7a99d81 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/CAL.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/CAL.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt /** * Calibration pair used by the linear calibration engine: a fingerstick reference value @@ -15,7 +15,7 @@ data class CAL( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var fingerstickMgdl: Double, var sensorMgdlAtPairing: Double ) : HasIDs, TimeStamped diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/DS.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DS.kt similarity index 79% rename from core/data/src/main/kotlin/app/aaps/core/data/model/DS.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/DS.kt index dca4d7d898ef..a384fe252127 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/DS.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DS.kt @@ -1,12 +1,12 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class DS( var id: Long = 0, var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var device: String? = null, var pump: String? = null, var enacted: String? = null, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/EB.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/EB.kt similarity index 91% rename from core/data/src/main/kotlin/app/aaps/core/data/model/EB.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/EB.kt index 11669e2947b5..77f462e9af91 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/EB.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/EB.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class EB( override var id: Long = 0, @@ -10,7 +10,7 @@ data class EB( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), /** Duration in milliseconds */ var duration: Long, var amount: Double, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/EPS.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/EPS.kt similarity index 95% rename from core/data/src/main/kotlin/app/aaps/core/data/model/EPS.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/EPS.kt index f6e14f45b6a2..24aec79fc856 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/EPS.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/EPS.kt @@ -2,7 +2,7 @@ package app.aaps.core.data.model import app.aaps.core.data.model.data.Block import app.aaps.core.data.model.data.TargetBlock -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class EPS( override var id: Long = 0, @@ -12,7 +12,7 @@ data class EPS( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var basalBlocks: List, var isfBlocks: List, var icBlocks: List, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/FD.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/FD.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/FD.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/FD.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/GV.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/GV.kt similarity index 81% rename from core/data/src/main/kotlin/app/aaps/core/data/model/GV.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/GV.kt index 0c6e6da546ce..ab8222294fc3 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/GV.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/GV.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class GV( override var id: Long = 0, @@ -10,7 +10,7 @@ data class GV( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var raw: Double?, var value: Double, var trendArrow: TrendArrow, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/GlucoseUnit.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/GlucoseUnit.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/GlucoseUnit.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/GlucoseUnit.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/HR.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/HR.kt similarity index 89% rename from core/data/src/main/kotlin/app/aaps/core/data/model/HR.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/HR.kt index 296c35e99f5c..842968faf3a3 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/HR.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/HR.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt /** Heart rate values measured by a user smartwatch or the like. */ data class HR( @@ -13,7 +13,7 @@ data class HR( var beatsPerMinute: Double, /** Source device that measured the heart rate. */ var device: String, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var version: Int = 0, var dateCreated: Long = -1, var isValid: Boolean = true, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/HasIDs.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/HasIDs.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/HasIDs.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/HasIDs.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/ICfg.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/ICfg.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/IDs.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/IDs.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/IDs.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/IDs.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/NE.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/NE.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/NE.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/NE.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/PS.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/PS.kt similarity index 95% rename from core/data/src/main/kotlin/app/aaps/core/data/model/PS.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/PS.kt index 3f6bd5be0c7e..1549e728c968 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/PS.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/PS.kt @@ -2,7 +2,7 @@ package app.aaps.core.data.model import app.aaps.core.data.model.data.Block import app.aaps.core.data.model.data.TargetBlock -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class PS( override var id: Long = 0, @@ -12,7 +12,7 @@ data class PS( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var basalBlocks: List, var isfBlocks: List, var icBlocks: List, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/RM.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/RM.kt similarity index 96% rename from core/data/src/main/kotlin/app/aaps/core/data/model/RM.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/RM.kt index cf30b4213941..b1da8e025979 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/RM.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/RM.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class RM( override var id: Long = 0, @@ -10,7 +10,7 @@ data class RM( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), /** Current running mode. */ var mode: Mode, /** Planned duration in milliseconds */ diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SC.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SC.kt similarity index 92% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SC.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SC.kt index b03a68eb3317..3116ff38d2fd 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/SC.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SC.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt /** Steps count values measured by a user smartwatch or the like. */ data class SC( @@ -18,7 +18,7 @@ data class SC( var steps180min: Int, /** Source device that measured the steps count. */ var device: String, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), override var version: Int = 0, override var dateCreated: Long = -1, override var isValid: Boolean = true, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/Scene.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/Scene.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/Scene.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/Scene.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SceneAction.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneAction.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SceneAction.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneAction.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SceneEndAction.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneEndAction.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SceneEndAction.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneEndAction.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SceneLifecycle.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneLifecycle.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SceneLifecycle.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SceneLifecycle.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SourceSensor.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensor.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SourceSensor.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/SourceSensorExtensions.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TB.kt similarity index 86% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TB.kt index 08e89c8e225e..36c2dc3d5e75 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TB.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class TB( override var id: Long = 0, @@ -10,7 +10,7 @@ data class TB( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var type: Type, var isAbsolute: Boolean, var rate: Double, @@ -51,9 +51,6 @@ data class TB( } } - val isInProgress: Boolean - get() = System.currentTimeMillis() in timestamp..timestamp + duration - val end get() = timestamp + duration diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TDD.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TDD.kt similarity index 86% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TDD.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TDD.kt index ad697fdf5438..2af742b53242 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/TDD.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TDD.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class TDD( override var id: Long = 0, @@ -10,7 +10,7 @@ data class TDD( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var basalAmount: Double = 0.0, var bolusAmount: Double = 0.0, var totalAmount: Double = 0.0, // if zero it's calculated as basalAmount + bolusAmount diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TE.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TE.kt similarity index 98% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TE.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TE.kt index f4c168e4c3fd..8ee4ec950635 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/TE.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TE.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class TE( override var id: Long = 0, @@ -10,7 +10,7 @@ data class TE( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), /** Duration in milliseconds */ var duration: Long = 0, var type: Type, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TT.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TT.kt similarity index 93% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TT.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TT.kt index 3e2ff6480cf6..d4fe67ca603e 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/TT.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TT.kt @@ -1,6 +1,6 @@ package app.aaps.core.data.model -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class TT( override var id: Long = 0, @@ -10,7 +10,7 @@ data class TT( override var referenceId: Long? = null, override var ids: IDs = IDs(), override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var reason: Reason, var highTarget: Double, // in mgdl var lowTarget: Double, // in mgdl diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TTPreset.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TTPreset.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TTPreset.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TTPreset.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TimeStamped.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TimeStamped.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TimeStamped.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TimeStamped.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TrendArrow.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/TrendArrow.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/TrendArrow.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/TrendArrow.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/UE.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/UE.kt similarity index 76% rename from core/data/src/main/kotlin/app/aaps/core/data/model/UE.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/UE.kt index 1bfed86b6f31..09cc8daa3cd7 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/UE.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/UE.kt @@ -3,12 +3,12 @@ package app.aaps.core.data.model import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources import app.aaps.core.data.ue.ValueWithUnit -import java.util.TimeZone +import app.aaps.core.data.time.systemUtcOffsetAt data class UE( var id: Long = 0L, override var timestamp: Long, - var utcOffset: Long = TimeZone.getDefault().getOffset(timestamp).toLong(), + var utcOffset: Long = systemUtcOffsetAt(timestamp), var action: Action, var source: Sources, var note: String, diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/data/Block.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/data/Block.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/data/TargetBlock.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/model/data/TargetBlock.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/plugin/PluginType.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/plugin/PluginType.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/plugin/PluginType.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/plugin/PluginType.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/Capability.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/Capability.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/Capability.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/Capability.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseSettings.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/DoseSettings.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseSettings.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/DoseSettings.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/DoseStepSize.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/ManufacturerType.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpCapability.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpDescription.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpTempBasalType.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpTempBasalType.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpTempBasalType.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpTempBasalType.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpType.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/PumpType.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/PumpType.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/pump/defs/TimeChangeType.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/TimeChangeType.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/pump/defs/TimeChangeType.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/pump/defs/TimeChangeType.kt diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/time/SystemTimeZone.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/time/SystemTimeZone.kt new file mode 100644 index 000000000000..121ea8d00d9d --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/time/SystemTimeZone.kt @@ -0,0 +1,13 @@ +package app.aaps.core.data.time + +/** + * Time zone offset in milliseconds for a given moment, DST aware. + * + * The records keep this as `utcOffset`. It is written to the database, it takes part in + * `contentEqualsTo` (so a changed value makes a record look different and sync again), it is sent to + * Nightscout - which validates it and answers 400 when it is wrong - and it goes to Open Humans. + * So the value has to stay exactly what the old `TimeZone.getDefault().getOffset(timestamp)` + * produced. Keeping it as a platform function does that at every call site at once, without + * touching the models or the ~154 places that already pass `utcOffset` themselves. + */ +expect fun systemUtcOffsetAt(timestamp: Long): Long diff --git a/core/data/src/main/kotlin/app/aaps/core/data/time/T.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/time/T.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/time/T.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/time/T.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/ue/Action.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/ue/Action.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/ue/Action.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/ue/Action.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/ue/Sources.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/ue/Sources.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/ue/ValueWithUnit.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/ue/ValueWithUnit.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/ue/ValueWithUnit.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/ue/ValueWithUnit.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/ui/ConfirmationLine.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/ui/ConfirmationLine.kt similarity index 100% rename from core/data/src/main/kotlin/app/aaps/core/data/ui/ConfirmationLine.kt rename to core/data/src/commonMain/kotlin/app/aaps/core/data/ui/ConfirmationLine.kt diff --git a/core/data/src/main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt b/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt similarity index 67% rename from core/data/src/main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt rename to core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt index 31ec568c4832..eec8d27f15c1 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.kt +++ b/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt @@ -5,23 +5,11 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale -/** - * Renders a [NumberFormat] as text. - * - * This is the only place in the app that still uses a platform number formatter. Everything else - * works with [NumberFormat], which is pure Kotlin. When this module is built for other platforms, - * this object becomes the platform specific part and nothing else has to change. - * - * Output is the same as the old `java.text.DecimalFormat` patterns: - * grouping separators are off, and rounding is half-even. - */ -object NumberFormatPlatform { - - /** Decimal separator for text that must not depend on the locale, for example server data. */ - const val SEPARATOR_DOT = '.' - - /** Decimal separator of the current locale. */ - val localeSeparator: Char get() = DecimalFormatSymbols.getInstance().decimalSeparator +actual object NumberFormatPlatform { + + actual val SEPARATOR_DOT: Char = '.' + + actual val localeSeparator: Char get() = DecimalFormatSymbols.getInstance().decimalSeparator // The locale is part of the key. A cached formatter keeps ALL the symbols of the locale it was // built with, not only the decimal separator - the minus sign and the digit shapes too. The app @@ -37,9 +25,9 @@ object NumberFormatPlatform { override fun initialValue(): MutableMap = HashMap() } - fun format(format: NumberFormat, value: Double): String = format(format, value, localeSeparator) + actual fun format(format: NumberFormat, value: Double): String = format(format, value, localeSeparator) - fun format(format: NumberFormat, value: Double, separator: Char): String = + actual fun format(format: NumberFormat, value: Double, separator: Char): String = formatterFor(Key(format, separator, Locale.getDefault())).format(value) private fun formatterFor(key: Key): DecimalFormat = diff --git a/core/data/src/jvmMain/kotlin/app/aaps/core/data/time/SystemTimeZone.jvm.kt b/core/data/src/jvmMain/kotlin/app/aaps/core/data/time/SystemTimeZone.jvm.kt new file mode 100644 index 000000000000..df1876dc6f9d --- /dev/null +++ b/core/data/src/jvmMain/kotlin/app/aaps/core/data/time/SystemTimeZone.jvm.kt @@ -0,0 +1,7 @@ +package app.aaps.core.data.time + +import java.util.TimeZone + +/** Exactly what the models did before, so the stored `utcOffset` does not change. */ +actual fun systemUtcOffsetAt(timestamp: Long): Long = + TimeZone.getDefault().getOffset(timestamp).toLong() diff --git a/core/data/src/test/kotlin/app/aaps/core/data/format/NumberFormatTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/format/NumberFormatTest.kt similarity index 100% rename from core/data/src/test/kotlin/app/aaps/core/data/format/NumberFormatTest.kt rename to core/data/src/jvmTest/kotlin/app/aaps/core/data/format/NumberFormatTest.kt diff --git a/core/data/src/test/kotlin/app/aaps/core/data/model/ICfgTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/ICfgTest.kt similarity index 100% rename from core/data/src/test/kotlin/app/aaps/core/data/model/ICfgTest.kt rename to core/data/src/jvmTest/kotlin/app/aaps/core/data/model/ICfgTest.kt diff --git a/core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt similarity index 100% rename from core/data/src/test/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt rename to core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt diff --git a/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt new file mode 100644 index 000000000000..cd8d0d02be29 --- /dev/null +++ b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt @@ -0,0 +1,53 @@ +package app.aaps.core.data.format + +import kotlin.math.abs +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.truncate + +/** + * Experiment only. + * + * mingwX64 exists here to prove that `commonMain` holds no JVM API. It is not a shipping target, so + * this actual does the simple thing rather than reaching for the platform locale data. A real iOS + * actual would use `NSNumberFormatter`, which is backed by the same CLDR data as the JVM one. + */ +actual object NumberFormatPlatform { + + actual val SEPARATOR_DOT: Char = '.' + + /** No locale data on this target, so always a dot. */ + actual val localeSeparator: Char = '.' + + actual fun format(format: NumberFormat, value: Double): String = format(format, value, localeSeparator) + + actual fun format(format: NumberFormat, value: Double, separator: Char): String { + if (value.isNaN()) return "NaN" + if (value.isInfinite()) return if (value > 0) "∞" else "-∞" + + val negative = value < 0 || (value == 0.0 && 1.0 / value < 0) + val scale = 10.0.pow(format.maxFractionDigits) + val scaled = abs(value) * scale + // half-even, same as DecimalFormat + val floor = truncate(scaled) + val rest = scaled - floor + val rounded = when { + rest > 0.5 -> floor + 1 + rest < 0.5 -> floor + floor.toLong() % 2L == 0L -> floor + else -> floor + 1 + } + + var whole = truncate(rounded / scale).toLong().toString() + var fraction = round(rounded - truncate(rounded / scale) * scale).toLong().toString() + if (format.maxFractionDigits > 0) fraction = fraction.padStart(format.maxFractionDigits, '0') + else fraction = "" + // drop trailing zeros above the minimum + while (fraction.length > format.minFractionDigits && fraction.endsWith('0')) + fraction = fraction.dropLast(1) + + whole = whole.padStart(format.minIntegerDigits, '0') + val sign = if (negative && (whole.any { it != '0' } || fraction.any { it != '0' })) "-" else "" + return if (fraction.isEmpty()) "$sign$whole" else "$sign$whole$separator$fraction" + } +} diff --git a/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/time/SystemTimeZone.mingw.kt b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/time/SystemTimeZone.mingw.kt new file mode 100644 index 000000000000..f0784c472fa5 --- /dev/null +++ b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/time/SystemTimeZone.mingw.kt @@ -0,0 +1,8 @@ +package app.aaps.core.data.time + +/** + * Experiment only - see [app.aaps.core.data.format.NumberFormatPlatform]. + * + * A real iOS actual would use `NSTimeZone.localTimeZone.secondsFromGMTForDate`. + */ +actual fun systemUtcOffsetAt(timestamp: Long): Long = 0L From 1aed547f7ada61ebffff43060f9abbe8c8e2f362 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 5 Aug 2026 22:11:56 +0200 Subject: [PATCH 003/146] cleanup --- core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt b/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt index 08e89c8e225e..e2a1061795aa 100644 --- a/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt +++ b/core/data/src/main/kotlin/app/aaps/core/data/model/TB.kt @@ -51,9 +51,6 @@ data class TB( } } - val isInProgress: Boolean - get() = System.currentTimeMillis() in timestamp..timestamp + duration - val end get() = timestamp + duration From e24476237b06329b0ba398f6a29baa3fd7bf7668 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 09:18:21 +0200 Subject: [PATCH 004/146] Prepare :core:nssdk --- _docs/KMP_IOS_FEASIBILITY.md | 106 +++++++- core/nssdk/build.gradle.kts | 1 - .../aaps/core/nssdk/NSAndroidClientImpl.kt | 1 - .../aaps/core/nssdk/NSAndroidRxClientImpl.kt | 15 -- .../nssdk/interfaces/NSAndroidRxClient.kt | 13 - .../nssdk/remotemodel/RemoteDeviceStatus.kt | 44 ++-- .../core/nssdk/remotemodel/RemoteEntry.kt | 30 +-- .../aaps/core/nssdk/remotemodel/RemoteFood.kt | 16 +- .../nssdk/remotemodel/RemoteProfileStore.kt | 8 +- .../nssdk/remotemodel/RemoteStatusResponse.kt | 8 +- .../core/nssdk/remotemodel/RemoteTreatment.kt | 4 +- .../nssdk/mapper/DeviceStatusMapperTest.kt | 232 ++++++++++++++++++ .../mapper/RealNightscoutTreatmentTest.kt | 185 ++++++++++++++ .../extensions/UtcOffsetUnitsTest.kt | 112 +++++++++ 14 files changed, 686 insertions(+), 89 deletions(-) delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidRxClientImpl.kt delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidRxClient.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/UtcOffsetUnitsTest.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index c8a9f6d72e6c..883c280d05f9 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -31,7 +31,7 @@ Multiplatform for the UI**, done step by step, starting with a small working sli | Room | 46 DAOs, **0** RxJava return types, 22 `suspend`, already on `BundledSQLiteDriver` | This is exactly the Room KMP setup | | Compose | **0** XML layouts in `:core:ui`, 2 left in `:ui` | Compose Multiplatform can use this | | `LocalContext.current` | 7 in `:core:ui`, 8 in `:ui` | Very small coupling to Android | -| Network code | 6 files touch Retrofit / OkHttp / socket.io | Small enough to rewrite on Ktor | +| Network code | 5 files touch Retrofit / OkHttp, 1 touches socket.io | REST part is small enough for Ktor | | kotlinx.serialization | 64 files (Gson: 34) | Already the main choice | | kotlinx-datetime | Declared in `libs.versions.toml` | Ready to use | | `androidx.lifecycle` ViewModel | Multiplatform since 2.8 | Most of the 129 uses are fine | @@ -58,7 +58,7 @@ So most of the Compose work of the last year can be reused. | Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | | RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | | Retrofit + OkHttp | 6 | Ktor client | -| socket.io-client | 1 (`NSClientV3Service`) | See warning below | +| socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | | Gson | 34 | kotlinx.serialization | | WorkManager | 44 | See warning below | | joda-time | 5 | kotlinx-datetime | @@ -85,8 +85,8 @@ Fragment / AppCompatActivity (12), and `R.string` (~136 `:ui`, 61 `:core:ui`, 60 ### Two that are not only porting cost -- **socket.io** - Ktor gives WebSockets but not the Socket.IO / Engine.IO protocol on top. Either - reimplement the handshake, or use polling. +- **socket.io** - not a library choice, a protocol the server dictates. See section 3a; it is the + one entry in the table above that must be **kept and abstracted**, not replaced. - **WorkManager** - iOS has no equivalent. `BGTaskScheduler` only gives wake ups that the system may delay for hours. A follower that expects to run every 5 minutes cannot work that way. The real answer is push (APNs) from a server, which changes the design, not only the code. @@ -107,6 +107,104 @@ Fragment / AppCompatActivity (12), and `R.string` (~136 `:ui`, 61 `:core:ui`, 60 --- +## 3a. socket.io - keep it, do not replace it + +**Nightscout runs a Socket.IO server.** That is the reason AAPS uses a Socket.IO client, and it is +not a choice this project gets to revisit. + +Socket.IO is not "WebSocket with a helper library". It is its own protocol on top: Engine.IO +framing, a handshake, namespaces, acks and its own reconnect rules. **A plain Ktor WebSocket client +cannot talk to a Socket.IO server at all.** Polling is not a fallback either - dropping the socket +means giving up push updates, which is the whole point of NSClientV3. + +So this dependency is **abstracted, not removed**: + +| Where | What | +| --- | --- | +| Android | keep `io.socket:socket.io-client:2.1.2` **unchanged** | +| iOS | `socket.io-client-swift` | +| shared | a small `expect interface` over the calls actually used | + +Both clients are written by the Socket.IO project itself, so protocol compatibility follows the +server rather than a third party's reimplementation. That matters more here than saving a +dependency. + +**The API surface is tiny.** `NSClientV3Service` uses only: + +``` +IO.socket(url) · .on(event, listener) · .off(event, listener) +.connect() · .disconnect() · .emit(..., Ack) +Socket.EVENT_CONNECT / EVENT_DISCONNECT +``` + +with the events `create`, `update`, `delete`, `announcement`, `alarm`, `urgent_alarm`, +`clear_alarm`. About 30 lines of `expect` / `actual` covers it. + +Kotlin Multiplatform Socket.IO libraries do exist - [moko-socket-io], [KotSock] (both wrap the same +two native clients), and pure Kotlin ports such as [dyte-io/socketio-kotlin] and [kmp-socketio]. For +this API surface a third party wrapper buys about 30 lines while adding a dependency on the sync +path. A pure Kotlin port is worse: it would **replace the working Android client** with a +reimplementation, which is the wrong direction of risk for a medical app. + +### Where to look first + +Since the plan is to write our own small wrapper rather than depend on one, the useful reading is +the existing wrappers' source, because they already are the `expect` / `actual` we would write: + +| Link | Why | +| --- | --- | +| [moko-socket-io] | Cleanest reference. Small, `expect class Socket` over the two native clients. | +| [KotSock] | Same approach, a second opinion on the API shape. | +| [moko-socket-io-sample] | A working KMP app using it, by the moko maintainer. Shows the CocoaPods wiring for `socket.io-client-swift`. | +| [KMP Socket.IO deep dive] | Walks through building exactly this kind of wrapper. Closest thing to the POC we would be reproducing. | + +**Check the dates before trusting any of them as a dependency.** moko-socket-io states Gradle 6.8+, +Android API 16+, iOS 11.0+ - those baselines are from around 2021, while this project is on Gradle +9.6.1 and Kotlin 2.4.10. That is a warning sign for a live dependency on the sync path, and no +problem at all for reading it as a pattern. + +[moko-socket-io]: https://github.com/icerockdev/moko-socket-io +[moko-socket-io-sample]: https://github.com/Alex009/moko-socket-io-sample +[KotSock]: https://github.com/whiterabb17/KotSock +[dyte-io/socketio-kotlin]: https://github.com/dyte-io/socketio-kotlin +[kmp-socketio]: https://klibs.io/project/HackWebRTC/kmp-socketio +[KMP Socket.IO deep dive]: https://rahuljindaltech.medium.com/building-cross-platform-libraries-with-kotlin-multiplatform-kmp-kmm-a-deep-dive-into-socket-io-89e58c3b221c + +### Protocol versions - checked + +Socket.IO major versions are **not** wire compatible, so the server and both clients have to agree. +Checked on 2026-08-05: + +| Part | Version | Protocol | +| --- | --- | --- | +| Nightscout `cgm-remote-monitor` 15.0.7 | `socket.io ~4.5.4` | **v4** | +| AAPS today, Android | `io.socket:socket.io-client:2.1.2` | v3 / v4 - correct | +| iOS should use | `Socket.IO-Client-Swift` **16.x** | v3 / v4 | +| moko-socket-io 0.6.0, iOS half | `Socket.IO-Client-Swift ~> 15.2.0` | **v2 - will not talk to Nightscout** | + +`socket.io-client-java` 2.x speaks Socket.IO 3 / 4. On the Swift side that generation only arrives +in **16.0.0** (February 2024, "now supports Socket.IO 3 servers"); 15.x is Socket.IO 2 only. + +So **moko-socket-io cannot be used against Nightscout as it is**, and simply bumping its pod to 16.x +is not a fix either - v15 to v16 had breaking API changes (there is an official `15to16` migration +guide), so moko's own wrapper code would not compile against it. + +Writing our own wrapper avoids the whole question, because we pick both versions ourselves: +`socket.io-client:2.1.2` unchanged on Android, `Socket.IO-Client-Swift` 16.x on iOS. + +Useful escape hatch: the Swift 16.x client can still reach a Socket.IO 2 server by passing +`.version(.two)` to the manager. So 16.x is the right choice even for someone running an old +Nightscout. + +### One more thing to check before writing it + +**The two clients may not behave the same.** `NSClientV3Service` already carries a comment that +*"java io.client doesn't support multiplexing, create 2 sockets"*, and a leak note about +`Manager.nsps` being a process-static, never-pruned cache. Whether the Swift client shares those +quirks is exactly what an `expect` / `actual` boundary hides until it bites. + +--- + ## 4. Options ### A - Native SwiftUI client, no shared code diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 62e0973fb1cb..84d96e175e3e 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { api(platform(libs.kotlinx.coroutines.bom)) api(libs.kotlinx.coroutines.core) runtimeOnly(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.rx3) api(platform(libs.kotlinx.serialization.bom)) api(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt index 25880d90dfad..af6a7cfd1223 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt @@ -43,7 +43,6 @@ import org.json.JSONObject /** * * This client uses suspend functions and therefore is only visible in Kotlin (@JvmSynthetic). - * An RxJava version can be found here [NSAndroidRxClientImpl] * * @param baseUrl the baseURL of the NightScout Instance * @param accessToken the access token of a role found in the admin panel of the NightScout instance diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidRxClientImpl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidRxClientImpl.kt deleted file mode 100644 index 57f8ae056889..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidRxClientImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.aaps.core.nssdk - -import app.aaps.core.nssdk.interfaces.NSAndroidClient -import app.aaps.core.nssdk.interfaces.NSAndroidRxClient -import app.aaps.core.nssdk.localmodel.Status -import app.aaps.core.nssdk.remotemodel.LastModified -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.rx3.rxSingle - -class NSAndroidRxClientImpl(private val client: NSAndroidClient) : NSAndroidRxClient { - - override fun getVersion(): Single = rxSingle { client.getVersion() } - override fun getStatus(): Single = rxSingle { client.getStatus() } - override fun getLastModified(): Single = rxSingle { client.getLastModified() } -} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidRxClient.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidRxClient.kt deleted file mode 100644 index 94ae49f1d651..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidRxClient.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.aaps.core.nssdk.interfaces - -import app.aaps.core.nssdk.localmodel.Status -import app.aaps.core.nssdk.remotemodel.LastModified -import io.reactivex.rxjava3.core.Single - -interface NSAndroidRxClient { - - fun getVersion(): Single - fun getStatus(): Single - fun getLastModified(): Single -} - diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt index c828af5eabcf..44d1805ee794 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt @@ -17,43 +17,43 @@ internal data class RemoteDeviceStatus( val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). @SerializedName("created_at") val createdAt: String? = null, // string or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerializedName("date") val date: Long?, // date as milliseconds - @SerializedName("uploaderBattery") val uploaderBattery: Int?,// integer($int64) - @SerializedName("isCharging") val isCharging: Boolean?, - @SerializedName("device") val device: String?, // "openaps://samsung SM-G970F" - - @SerializedName("uploader") val uploader: Uploader?, - @SerializedName("pump") val pump: Pump?, - @SerializedName("openaps") val openaps: OpenAps? + @SerializedName("date") val date: Long? = null, // date as milliseconds + @SerializedName("uploaderBattery") val uploaderBattery: Int? = null,// integer($int64) + @SerializedName("isCharging") val isCharging: Boolean? = null, + @SerializedName("device") val device: String? = null, // "openaps://samsung SM-G970F" + + @SerializedName("uploader") val uploader: Uploader? = null, + @SerializedName("pump") val pump: Pump? = null, + @SerializedName("openaps") val openaps: OpenAps? = null ) { data class Pump( - @SerializedName("clock") val clock: String?, // timestamp in ISO - @SerializedName("reservoir") val reservoir: Double?, - @SerializedName("reservoir_display_override") val reservoirDisplayOverride: String?, - @SerializedName("battery") val battery: Battery?, - @SerializedName("status") val status: Status?, - @SerializedName("extended") val extended: JsonObject? // Gson, content depending on pump driver + @SerializedName("clock") val clock: String? = null, // timestamp in ISO + @SerializedName("reservoir") val reservoir: Double? = null, + @SerializedName("reservoir_display_override") val reservoirDisplayOverride: String? = null, + @SerializedName("battery") val battery: Battery? = null, + @SerializedName("status") val status: Status? = null, + @SerializedName("extended") val extended: JsonObject? = null // Gson, content depending on pump driver ) { data class Battery( - @SerializedName("percent") val percent: Int?, - @SerializedName("voltage") val voltage: Double? + @SerializedName("percent") val percent: Int? = null, + @SerializedName("voltage") val voltage: Double? = null ) data class Status( - @SerializedName("status") val status: String?, - @SerializedName("timestamp") val timestamp: String? + @SerializedName("status") val status: String? = null, + @SerializedName("timestamp") val timestamp: String? = null ) } data class OpenAps( - @SerializedName("suggested") val suggested: JsonObject?, // Gson - @SerializedName("enacted") val enacted: JsonObject?, // Gson - @SerializedName("iob") val iob: JsonObject? // Gson + @SerializedName("suggested") val suggested: JsonObject? = null, // Gson + @SerializedName("enacted") val enacted: JsonObject? = null, // Gson + @SerializedName("iob") val iob: JsonObject? = null // Gson ) data class Uploader( - @SerializedName("battery") val battery: Int? + @SerializedName("battery") val battery: Int? = null ) } diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt index 0baa62f697a6..d5064f7f6507 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt @@ -13,26 +13,26 @@ import com.google.gson.annotations.SerializedName * */ internal data class RemoteEntry( @SerializedName("type") val type: String, // sgv, mbg, cal, etc; Bolus type NORMAL, SMB, PRIMING - @SerializedName("sgv") val sgv: Double?, // number The glucose reading. (only available for sgv types) + @SerializedName("sgv") val sgv: Double? = null, // number The glucose reading. (only available for sgv types) @SerializedName("dateString") val dateString: String? = null, - @SerializedName("date") var date: Long?, // required ? TODO: date and dateString are redundant - are both needed? how to handle inconsistency then? Only expose one to clients? - @SerializedName("device") val device: String?, // The device from which the data originated (including serial number of the device, if it is relevant and safe). - @SerializedName("direction") val direction: String?, // TODO: what implicit convention for the directions exists? - @SerializedName("identifier") val identifier: String?, - @SerializedName("srvModified") val srvModified: Long?, - @SerializedName("srvCreated") val srvCreated: Long?, + @SerializedName("date") var date: Long? = null, // required ? TODO: date and dateString are redundant - are both needed? how to handle inconsistency then? Only expose one to clients? + @SerializedName("device") val device: String? = null, // The device from which the data originated (including serial number of the device, if it is relevant and safe). + @SerializedName("direction") val direction: String? = null, // TODO: what implicit convention for the directions exists? + @SerializedName("identifier") val identifier: String? = null, + @SerializedName("srvModified") val srvModified: Long? = null, + @SerializedName("srvCreated") val srvCreated: Long? = null, // Philoul Others fields below found in API v3 doc @SerializedName("app") var app: String? = null, - @SerializedName("utcOffset") var utcOffset: Long?, // Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is + @SerializedName("utcOffset") var utcOffset: Long? = null, // Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is // automatically parsed from the date field. - @SerializedName("subject") val subject: String?, // Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. + @SerializedName("subject") val subject: String? = null, // Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. @SerializedName("modifiedBy") val modifiedBy: String? = null, // Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. - @SerializedName("isValid") val isValid: Boolean?, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) - @SerializedName("isReadOnly") val isReadOnly: Boolean?, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerializedName("noise") val noise: Double?, // 0 or 1 found in the export, I don't know if other values possible ? - @SerializedName("filtered") val filtered: Double?, // The raw filtered value directly from CGM transmitter. (only available for sgv types) - @SerializedName("unfiltered") val unfiltered: Double?, // The raw unfiltered value directly from CGM transmitter. (only available for sgv types) - @SerializedName("units") val units: String?, // The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + @SerializedName("isValid") val isValid: Boolean? = null, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) + @SerializedName("isReadOnly") val isReadOnly: Boolean? = null, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. + @SerializedName("noise") val noise: Double? = null, // 0 or 1 found in the export, I don't know if other values possible ? + @SerializedName("filtered") val filtered: Double? = null, // The raw filtered value directly from CGM transmitter. (only available for sgv types) + @SerializedName("unfiltered") val unfiltered: Double? = null, // The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + @SerializedName("units") val units: String? = null, // The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. @SerializedName("mbg") val mbg: Double? = null, // Manual blood glucose reading (only available for mbg types). AAPS uses a marked mbg to carry a calibration fingerstick. // AAPS-specific calibration fields, carried on a marked `mbg` entry so a follower can re-fit the calibration curve. @SerializedName("sensorMgdlAtPairing") val sensorMgdlAtPairing: Double? = null, // sensor value at the moment the fingerstick was entered diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt index 5021c82ddda7..4970ade2823b 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt @@ -11,17 +11,17 @@ import com.google.gson.annotations.SerializedName **/ internal data class RemoteFood( @SerializedName("type") val type: String, // we are interesting in type "food" - @SerializedName("date") val date: Long?, + @SerializedName("date") val date: Long? = null, @SerializedName("name") val name: String, - @SerializedName("category") val category: String?, - @SerializedName("subcategory") val subcategory: String?, - @SerializedName("unit") val unit: String?, + @SerializedName("category") val category: String? = null, + @SerializedName("subcategory") val subcategory: String? = null, + @SerializedName("unit") val unit: String? = null, @SerializedName("portion") val portion: Double, @SerializedName("carbs") val carbs: Int, - @SerializedName("gi") val gi: Int?, - @SerializedName("energy") val energy: Int?, - @SerializedName("protein") val protein: Int?, - @SerializedName("fat") val fat: Int?, + @SerializedName("gi") val gi: Int? = null, + @SerializedName("energy") val energy: Int? = null, + @SerializedName("protein") val protein: Int? = null, + @SerializedName("fat") val fat: Int? = null, @SerializedName("identifier") val identifier: String?, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. @SerializedName("isValid") diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt index 3d6912553761..593d5f15f1fa 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt @@ -16,8 +16,8 @@ data class RemoteProfileStore( @SerializedName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. @SerializedName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). @SerializedName("created_at") val createdAt: String? = null, // string or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerializedName("date") val date: Long?, // date as milliseconds - @SerializedName("startDate") val startDate: Long?, // record valid from + @SerializedName("date") val date: Long? = null, // date as milliseconds + @SerializedName("startDate") val startDate: Long? = null, // record valid from @SerializedName("defaultProfile") val defaultProfile: String,// default profile in store //@Serializable(with = JSONSerializer::class) @@ -42,8 +42,8 @@ data class RemoteProfileStore( @Serializable data class ProfileEntry( @SerializedName("time") val time: String, - @SerializedName("timeAsSeconds") val timeAsSeconds: Long?, + @SerializedName("timeAsSeconds") val timeAsSeconds: Long? = null, @SerializedName("value") val value: Double ) */ -} \ No newline at end of file +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt index b9cc33544e41..e63e9ade05a2 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt @@ -18,10 +18,10 @@ internal data class RemoteStorage( ) internal data class RemoteCreateUpdateResponse( - @SerializedName("identifier") val identifier: String?, - @SerializedName("isDeduplication") val isDeduplication: Boolean?, - @SerializedName("deduplicatedIdentifier") val deduplicatedIdentifier: String?, - @SerializedName("lastModified") val lastModified: Long? + @SerializedName("identifier") val identifier: String? = null, + @SerializedName("isDeduplication") val isDeduplication: Boolean? = null, + @SerializedName("deduplicatedIdentifier") val deduplicatedIdentifier: String? = null, + @SerializedName("lastModified") val lastModified: Long? = null ) internal data class RemoteApiPermissions( diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt index a64c8081be39..7d1c9f5686c0 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt @@ -15,7 +15,7 @@ import org.joda.time.format.ISODateTimeFormat * * */ internal data class RemoteTreatment( - @SerializedName("identifier") val identifier: String?, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. + @SerializedName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. @SerializedName("date") var date: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') @SerializedName("mills") val mills: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix @SerializedName("timestamp") val timestamp: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') @@ -29,7 +29,7 @@ internal data class RemoteTreatment( @SerializedName("modifiedBy") val modifiedBy: String? = null, // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. @SerializedName("isValid") val isValid: Boolean? = null, // boolean A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) @SerializedName("isReadOnly") val isReadOnly: Boolean? = null, // boolean A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerializedName("eventType") val eventType: EventType?, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" + @SerializedName("eventType") val eventType: EventType? = null, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" @SerializedName("glucose") val glucose: Double? = null, // double Current glucose @SerializedName("glucoseType") val glucoseType: String? = null, // string example: "Sensor", "Finger", "Manual" @SerializedName("units") val units: String? = null, // string The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt new file mode 100644 index 000000000000..fe8fa18b8687 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt @@ -0,0 +1,232 @@ +package app.aaps.core.nssdk.mapper + +import com.google.common.truth.Truth.assertThat +import com.google.gson.JsonSyntaxException +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * Device status carries **schema-less** JSON: `pump.extended`, `openaps.suggested`, `openaps.enacted` + * and `openaps.iob` hold whatever the pump driver and the APS algorithm put there. AAPS does not + * know their shape, it only has to carry them through without losing anything. + * + * Today the wire model [app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus] holds those subtrees as + * **Gson** `JsonObject`, while the local model + * [app.aaps.core.nssdk.localmodel.devicestatus.NSDeviceStatus] already holds them as **kotlinx** + * `JsonObject`. `DeviceStatusMapper` bridges the two by turning a tree into text and parsing it + * again, in both directions. + * + * These tests assert on **parsed structure**, never on `toString()`. The existing + * `DeviceStatusExtensionKtTest` compares string form, which would fail after a move to kotlinx even + * if every value were preserved, because the two libraries do not print identically. + * + * The last test is the important one: since the schema is unknown, a rewrite that quietly drops keys + * it does not recognise would pass every other assertion here. + */ +class DeviceStatusMapperTest { + + private val deviceStatusJson = """ + { + "app": "AAPS", + "identifier": "abc123", + "device": "AndroidAPS-DanaRS", + "created_at": "2023-01-02T15:29:39.460Z", + "date": 1672673379460, + "uploaderBattery": 88, + "isCharging": false, + "pump": { + "clock": "2023-01-02T15:29:39.460Z", + "reservoir": 123.4, + "battery": { "percent": 75 }, + "status": { "status": "normal", "timestamp": "2023-01-02T15:20:20.656Z" }, + "extended": { + "Version": "3.1.0.3-dev", + "PumpIOB": 0.692, + "LastBolus": "2.1.2023 16:20:20", + "BaseBasalRate": 0.5, + "ActiveProfile": "MyProfile" + } + }, + "openaps": { + "suggested": { + "temp": "absolute", + "bg": 133, + "tick": -6, + "eventualBG": 67, + "insulinReq": 0, + "sensitivityRatio": 1, + "variable_sens": 97.5, + "predBGs": { + "IOB": [133, 127, 121, 116, 111], + "ZT": [133, 127, 121, 115, 110], + "UAM": [133, 127, 121, 115, 110] + }, + "reason": "COB: 0, Dev: 0.1, BGI: -0.3; minGuardBG 2.1<4.0", + "COB": 0, + "IOB": 0.692, + "duration": 90, + "rate": 0 + }, + "enacted": { "temp": "absolute", "duration": 90, "rate": 0, "received": true }, + "iob": { "iob": 0.692, "basaliob": -0.411, "activity": 0.0126 } + } + } + """.trimIndent() + + // ------------------------------------------------------------------ parsing + + @Test + fun `parses the flat fields`() { + val status = deviceStatusJson.toNSDeviceStatus() + assertThat(status.app).isEqualTo("AAPS") + assertThat(status.device).isEqualTo("AndroidAPS-DanaRS") + assertThat(status.uploaderBattery).isEqualTo(88) + assertThat(status.pump?.battery?.percent).isEqualTo(75) + assertThat(status.pump?.status?.status).isEqualTo("normal") + } + + @Test + fun `keeps the schema-less pump extended subtree`() { + val extended = deviceStatusJson.toNSDeviceStatus().pump?.extended + assertThat(extended).isNotNull() + assertThat(extended!!.keys).containsExactly( + "Version", "PumpIOB", "LastBolus", "BaseBasalRate", "ActiveProfile" + ) + assertThat(extended["Version"]?.jsonPrimitive?.content).isEqualTo("3.1.0.3-dev") + assertThat(extended["PumpIOB"]?.jsonPrimitive?.content).isEqualTo("0.692") + } + + @Test + fun `keeps nesting three levels deep`() { + val suggested = deviceStatusJson.toNSDeviceStatus().openaps?.suggested + assertThat(suggested).isNotNull() + + val predBGs = suggested!!["predBGs"]?.jsonObject + assertThat(predBGs).isNotNull() + assertThat(predBGs!!.keys).containsExactly("IOB", "ZT", "UAM") + + val iobCurve = predBGs["IOB"]!!.jsonArray + assertThat(iobCurve).hasSize(5) + assertThat(iobCurve[0].jsonPrimitive.content).isEqualTo("133") + assertThat(iobCurve[4].jsonPrimitive.content).isEqualTo("111") + } + + @Test + fun `keeps numbers exactly as sent`() { + val suggested = deviceStatusJson.toNSDeviceStatus().openaps?.suggested!! + // A whole number must not gain a decimal point, and a decimal must not lose precision. + assertThat(suggested["bg"]?.jsonPrimitive?.content).isEqualTo("133") + assertThat(suggested["COB"]?.jsonPrimitive?.content).isEqualTo("0") + assertThat(suggested["tick"]?.jsonPrimitive?.content).isEqualTo("-6") + assertThat(suggested["variable_sens"]?.jsonPrimitive?.content).isEqualTo("97.5") + assertThat(suggested["IOB"]?.jsonPrimitive?.content).isEqualTo("0.692") + assertThat(deviceStatusJson.toNSDeviceStatus().openaps?.iob?.get("basaliob")?.jsonPrimitive?.content) + .isEqualTo("-0.411") + } + + @Test + fun `keeps a long reason string with punctuation`() { + val suggested = deviceStatusJson.toNSDeviceStatus().openaps?.suggested!! + assertThat(suggested["reason"]?.jsonPrimitive?.content) + .isEqualTo("COB: 0, Dev: 0.1, BGI: -0.3; minGuardBG 2.1<4.0") + } + + // ------------------------------------------------------------------ round trip + + @Test + fun `subtrees survive a round trip through the wire format`() { + val first = deviceStatusJson.toNSDeviceStatus() + val second = first.convertToRemoteAndBack() + + assertSameTree(first.pump?.extended, second.pump?.extended) + assertSameTree(first.openaps?.suggested, second.openaps?.suggested) + assertSameTree(first.openaps?.enacted, second.openaps?.enacted) + assertSameTree(first.openaps?.iob, second.openaps?.iob) + + // and the nesting specifically + val before = first.openaps?.suggested!!["predBGs"]!!.jsonObject["IOB"]!!.jsonArray + val after = second.openaps?.suggested!!["predBGs"]!!.jsonObject["IOB"]!!.jsonArray + assertThat(after).hasSize(before.size) + assertThat(after[0].jsonPrimitive.content).isEqualTo(before[0].jsonPrimitive.content) + } + + @Test + fun `missing subtrees stay null`() { + val json = """{"app":"AAPS","device":"d","date":1672673379460}""" + val status = json.toNSDeviceStatus() + assertThat(status.pump).isNull() + assertThat(status.openaps).isNull() + } + + @Test + fun `an empty subtree stays empty and does not become null`() { + val json = """{"app":"AAPS","date":1,"openaps":{"suggested":{}}}""" + val openaps = json.toNSDeviceStatus().openaps + assertThat(openaps?.suggested).isNotNull() + assertThat(openaps?.suggested?.keys).isEmpty() + assertThat(openaps?.enacted).isNull() // absent key -> null + } + + /** + * An **explicit** `null` is not the same as an absent key here, and it throws. + * + * The field is declared `JsonObject?`, but Gson's adapter for the concrete `JsonObject` type + * rejects `JsonNull` instead of mapping it to Kotlin `null`. An absent key is fine, a written + * `null` is not. + * + * Pinned, not fixed - the caller `NSClientV3Service.onDataCreateUpdate` is a socket.io listener + * with no try/catch, so this escapes onto the socket callback thread. Worth noting that AAPS + * itself never writes `null` here, but nothing stops another uploader from doing so. + * + * kotlinx.serialization would accept it and give `null`, so a migration would **change** this. + * If that is wanted, change this test on purpose. + */ + @Test + fun `an explicit null subtree throws - current behaviour, pinned`() { + val json = """{"app":"AAPS","date":1,"openaps":{"suggested":{},"enacted":null}}""" + assertThrows { json.toNSDeviceStatus() } + } + + /** + * The point of holding these as raw JSON is that AAPS does not know the schema. A future + * Nightscout, a new pump driver or a newer AAPS may add keys this version has never seen, and + * they must still be carried through untouched. + */ + @Test + fun `unknown keys from a newer sender are preserved`() { + val json = """ + {"app":"AAPS","date":1, + "openaps":{"suggested":{ + "bg":133, + "somethingFromTheFuture":"keep me", + "nestedFuture":{"deep":{"deeper":[1,2,3]}} + }}} + """.trimIndent() + + val suggested = json.toNSDeviceStatus().openaps?.suggested!! + assertThat(suggested["somethingFromTheFuture"]?.jsonPrimitive?.content).isEqualTo("keep me") + assertThat(suggested["nestedFuture"]?.jsonObject?.get("deep")?.jsonObject?.get("deeper")?.jsonArray) + .hasSize(3) + + // and they must still be there after a trip through the wire model + val back = json.toNSDeviceStatus().convertToRemoteAndBack().openaps?.suggested!! + assertThat(back["somethingFromTheFuture"]?.jsonPrimitive?.content).isEqualTo("keep me") + assertThat(back["nestedFuture"]?.jsonObject?.get("deep")?.jsonObject?.get("deeper")?.jsonArray) + .hasSize(3) + } + + /** Compare two subtrees by content, never by printed form. */ + private fun assertSameTree(expected: JsonObject?, actual: JsonObject?) { + if (expected == null) { + assertThat(actual).isNull() + return + } + assertThat(actual).isNotNull() + assertThat(actual!!.keys).isEqualTo(expected.keys) + for (key in expected.keys) assertThat(actual[key]).isEqualTo(expected[key]) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt new file mode 100644 index 000000000000..8067c9c7564d --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt @@ -0,0 +1,185 @@ +package app.aaps.core.nssdk.mapper + +import app.aaps.core.nssdk.localmodel.treatment.NSBolus +import app.aaps.core.nssdk.localmodel.treatment.NSCarbs +import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryBasal +import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryTarget +import com.google.common.truth.Truth.assertThat +import com.google.gson.JsonSyntaxException +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * Parsing tests built from real payloads taken from a Nightscout test instance + * (`/api/v1/treatments.json`, AAPS uploader, Dana RS pump). + * + * Two jobs: + * + * 1. Pin what the current parser produces for well formed records. The wire layer is about to be + * rewritten (Gson to kotlinx.serialization, Retrofit to Ktor) and the output has to stay the + * same. These are characterization tests: if one fails after the rewrite, the rewrite is wrong, + * not the test. + * 2. Show that a record with fields missing does not crash. Nightscout data comes from many + * uploaders and many app versions, so almost every field can be absent. + * + * The payloads are real but the identifying parts were replaced: `pumpSerial` and `subject`. + */ +class RealNightscoutTreatmentTest { + + // ---------------------------------------------------------------- complete records + + @Test + fun `parses a real SMB correction bolus`() { + val json = """ + {"_id":"6a7413f54d1ba8c2e419e30e","app":"AAPS","date":1785992179555, + "eventType":"Correction Bolus", + "icfg":{"concentration":1,"insulinEndTime":32040000,"insulinLabel":"Novorapid (1) 75m 8.9h U100","insulinPeakTime":4500000}, + "insulin":0.25,"isBasalInsulin":false,"isReadOnly":false,"isValid":true, + "pumpId":1785992181495,"pumpSerial":"TESTSERIAL","pumpType":"DANA_RS","type":"SMB","utcOffset":120, + "created_at":"2026-08-06T04:56:19.555Z","identifier":"1bebd84b-8d61-58b2-a967-1b34a1e8c4d4", + "srvModified":1785992181588,"srvCreated":1785992181588,"subject":"tester", + "mills":1785992179555,"carbs":null} + """.trimIndent() + + val treatment = json.toNSTreatment() + assertThat(treatment).isInstanceOf(NSBolus::class.java) + treatment as NSBolus + assertThat(treatment.insulin).isEqualTo(0.25) + assertThat(treatment.date).isEqualTo(1785992179555) + assertThat(treatment.identifier).isEqualTo("1bebd84b-8d61-58b2-a967-1b34a1e8c4d4") + assertThat(treatment.isValid).isTrue() + } + + @Test + fun `parses a real temp basal`() { + val json = """ + {"_id":"6a740f444d1ba8c2e419e301","absolute":2,"app":"AAPS","date":1785990980390, + "duration":25,"durationInMilliseconds":1500265,"eventType":"Temp Basal", + "isReadOnly":false,"isValid":true,"pumpId":1785990980390,"pumpSerial":"TESTSERIAL", + "pumpType":"DANA_RS","rate":2,"type":"NORMAL","utcOffset":120, + "created_at":"2026-08-06T04:36:20.390Z","identifier":"cb180766-42ed-55cc-a253-5200d4e0c2c6", + "srvModified":1785992481019,"srvCreated":1785990980561,"subject":"tester", + "endId":1785992480655,"modifiedBy":"tester","endmills":1785992480655, + "carbs":null,"insulin":null} + """.trimIndent() + + val treatment = json.toNSTreatment() + assertThat(treatment).isInstanceOf(NSTemporaryBasal::class.java) + treatment as NSTemporaryBasal + assertThat(treatment.date).isEqualTo(1785990980390) + assertThat(treatment.duration).isEqualTo(1500265) + assertThat(treatment.isValid).isTrue() + } + + @Test + fun `parses a real temporary target`() { + val json = """ + {"_id":"6a741d764d1ba8c2e419e322","app":"AAPS","date":1785994614220, + "duration":60,"durationInMilliseconds":3600000,"eventType":"Temporary Target", + "isReadOnly":false,"isValid":true,"reason":"Automation", + "targetBottom":144.12472,"targetTop":144.12472,"units":"mg/dl","utcOffset":120, + "created_at":"2026-08-06T05:36:54.220Z","identifier":"0426fdb9-a59b-59c5-a556-31f08d5382d0", + "srvModified":1785994614241,"srvCreated":1785994614241,"subject":"tester", + "mills":1785994614220,"endmills":1785998214220,"carbs":null,"insulin":null} + """.trimIndent() + + val treatment = json.toNSTreatment() + assertThat(treatment).isInstanceOf(NSTemporaryTarget::class.java) + treatment as NSTemporaryTarget + assertThat(treatment.targetBottom).isEqualTo(144.12472) + assertThat(treatment.targetTop).isEqualTo(144.12472) + assertThat(treatment.date).isEqualTo(1785994614220) + } + + @Test + fun `parses a real carb correction with notes`() { + val json = """ + {"_id":"6a74011c4d1ba8c2e419e2de","app":"AAPS","carbs":4,"date":1785987360000, + "eventType":"Carb Correction","isReadOnly":false,"isValid":true,"notes":"Random carbs", + "utcOffset":120,"created_at":"2026-08-06T03:36:00.000Z", + "identifier":"752f76ab-86af-5de0-a5af-e488f8459a91", + "srvModified":1785987356851,"srvCreated":1785987356851,"subject":"tester", + "mills":1785987360000,"insulin":null} + """.trimIndent() + + val treatment = json.toNSTreatment() + assertThat(treatment).isInstanceOf(NSCarbs::class.java) + treatment as NSCarbs + assertThat(treatment.carbs).isEqualTo(4.0) + assertThat(treatment.notes).isEqualTo("Random carbs") + } + + // ---------------------------------------------------------------- round trip + + @Test + fun `bolus survives a round trip to the wire format and back`() { + val json = """ + {"eventType":"Correction Bolus","date":1785992179555,"insulin":0.25,"type":"SMB", + "isValid":true,"utcOffset":120,"identifier":"abc","app":"AAPS"} + """.trimIndent() + + val first = json.toNSTreatment() + assertThat(first).isNotNull() + val second = first!!.convertToRemoteAndBack() + assertThat(second).isEqualTo(first) + } + + // ---------------------------------------------------------------- missing fields must not crash + + /** + * Nightscout records come from many uploaders and many app versions. Anything can be absent. + * None of these may throw - either a treatment comes back, or null, but never an exception. + */ + @Test + fun `records with missing fields do not crash the parser`() { + val cases = listOf( + "empty object" to "{}", + "only eventType" to """{"eventType":"Correction Bolus"}""", + "bolus without insulin" to """{"eventType":"Correction Bolus","date":1785992179555,"isValid":true}""", + "bolus without date" to """{"eventType":"Correction Bolus","insulin":0.25,"isValid":true}""", + "temp basal without duration" to """{"eventType":"Temp Basal","date":1785990980390,"rate":2,"isValid":true}""", + "temp basal without rate" to """{"eventType":"Temp Basal","date":1785990980390,"duration":25,"isValid":true}""", + "carbs without amount" to """{"eventType":"Carb Correction","date":1785987360000,"isValid":true}""", + "target without values" to """{"eventType":"Temporary Target","date":1785994614220,"duration":60,"isValid":true}""", + "unknown eventType" to """{"eventType":"Something New","date":1785992179555,"isValid":true}""", + "no eventType at all" to """{"date":1785992179555,"insulin":0.25,"isValid":true}""", + "explicit nulls" to """{"eventType":"Correction Bolus","date":1785992179555,"insulin":null,"carbs":null,"notes":null,"isValid":true}""", + "no utcOffset" to """{"eventType":"Correction Bolus","date":1785992179555,"insulin":0.25,"isValid":true}""", + "no identifier" to """{"eventType":"Correction Bolus","date":1785992179555,"insulin":0.25,"isValid":true}""" + ) + + for ((name, json) in cases) + try { + json.toNSTreatment() // may return null, must not throw + } catch (e: Exception) { + throw AssertionError("Parsing '$name' threw ${e::class.simpleName}: ${e.message}", e) + } + } + + /** + * Malformed text **throws** today - `toNSTreatment()` is + * `Gson().fromJson(this, RemoteTreatment::class.java).toTreatment()` with no guard, even though + * its return type is nullable. This test records that, it does not endorse it. + * + * Pinned because the caller is unprotected: `NSClientV3Service.onDataCreateUpdate` is a + * socket.io listener with no try/catch, so whatever this throws escapes onto the socket + * callback thread. After the move to kotlinx.serialization the exception type would become + * `SerializationException`, and any `catch` further up would silently stop catching it. + * + * If the parser is ever made defensive, change this test on purpose - do not let a rewrite + * change it by accident. + */ + @Test + fun `malformed json throws - current behaviour, pinned`() { + assertThrows { "hello".toNSTreatment() } + assertThrows { """{"eventType":"Correction Bolus","date":""".toNSTreatment() } + assertThrows { """[{"eventType":"Correction Bolus"}]""".toNSTreatment() } + assertThrows { """{"eventType":"Correction Bolus","date":"not-a-number"}""".toNSTreatment() } + } + + /** Empty text is a separate case: Gson returns null, and the unguarded `.toTreatment()` then fails. */ + @Test + fun `empty text throws NullPointerException - current behaviour, pinned`() { + assertThrows { "".toNSTreatment() } + } +} diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/UtcOffsetUnitsTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/UtcOffsetUnitsTest.kt new file mode 100644 index 000000000000..1b0ce77e82e7 --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/UtcOffsetUnitsTest.kt @@ -0,0 +1,112 @@ +package app.aaps.plugins.sync.nsclientV3.extensions + +import app.aaps.core.data.model.CA +import app.aaps.core.data.model.GV +import app.aaps.core.data.model.IDs +import app.aaps.core.data.model.SourceSensor +import app.aaps.core.data.model.TrendArrow +import app.aaps.core.data.time.T +import app.aaps.core.nssdk.localmodel.treatment.NSCarbs +import app.aaps.core.nssdk.mapper.convertToRemoteAndBack +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +/** + * `utcOffset` is carried in **minutes by Nightscout** and in **milliseconds by AAPS**. + * + * Every converter in this package does the conversion: + * ``` + * inbound NS -> AAPS : utcOffset = T.mins(utcOffset ?: 0L).msecs() + * outbound AAPS -> NS : utcOffset = T.msecs(utcOffset).mins() + * ``` + * + * Losing that conversion would not crash and would not look obviously wrong in a log: a record + * would carry 120 instead of 7200000, which reads as two minutes instead of two hours. It would + * then go into `contentEqualsTo`, so every record would look changed and sync again, and the wrong + * value would be uploaded back to Nightscout. + * + * These tests pin the units so a rewrite of the wire layer cannot flatten them silently. + */ +internal class UtcOffsetUnitsTest { + + private val plusTwoHoursMs = 7_200_000L // CEST, what the test server reports as 120 + private val plusTwoHoursMin = 120L + + @Test + fun `carbs - AAPS milliseconds become Nightscout minutes`() { + val carbs = CA(timestamp = 10000, isValid = true, amount = 1.0, duration = 0, utcOffset = plusTwoHoursMs) + + assertThat(carbs.toNSCarbs().utcOffset).isEqualTo(plusTwoHoursMin) + } + + @Test + fun `carbs - a full round trip keeps the AAPS value`() { + val carbs = CA(timestamp = 10000, isValid = true, amount = 1.0, duration = 0, utcOffset = plusTwoHoursMs) + + val back = (carbs.toNSCarbs().convertToRemoteAndBack() as NSCarbs).toCarbs() + assertThat(back.utcOffset).isEqualTo(plusTwoHoursMs) + } + + @Test + fun `glucose value - AAPS milliseconds become Nightscout minutes and back`() { + val gv = GV( + timestamp = 10000, value = 120.0, isValid = true, utcOffset = plusTwoHoursMs, + raw = null, trendArrow = TrendArrow.FLAT, noise = null, + sourceSensor = SourceSensor.DEXCOM_G6_NATIVE, ids = IDs() + ) + + val ns = gv.toNSSvgV3() + assertThat(ns.utcOffset).isEqualTo(plusTwoHoursMin) + assertThat(ns.toGV().utcOffset).isEqualTo(plusTwoHoursMs) + } + + @Test + fun `offsets that are not whole hours survive`() { + // India is +05:30, Nepal +05:45, Chatham +12:45 - all whole minutes, none whole hours. + val cases = mapOf( + 19_800_000L to 330L, // +05:30 + 20_700_000L to 345L, // +05:45 + 45_900_000L to 765L // +12:45 + ) + for ((ms, minutes) in cases) { + val carbs = CA(timestamp = 10000, isValid = true, amount = 1.0, duration = 0, utcOffset = ms) + assertThat(carbs.toNSCarbs().utcOffset).isEqualTo(minutes) + assertThat((carbs.toNSCarbs().convertToRemoteAndBack() as NSCarbs).toCarbs().utcOffset).isEqualTo(ms) + } + } + + @Test + fun `negative and zero offsets survive`() { + val cases = mapOf( + 0L to 0L, + -18_000_000L to -300L, // -05:00, US Eastern standard time + -34_200_000L to -570L // -09:30, Marquesas + ) + for ((ms, minutes) in cases) { + val carbs = CA(timestamp = 10000, isValid = true, amount = 1.0, duration = 0, utcOffset = ms) + assertThat(carbs.toNSCarbs().utcOffset).isEqualTo(minutes) + assertThat((carbs.toNSCarbs().convertToRemoteAndBack() as NSCarbs).toCarbs().utcOffset).isEqualTo(ms) + } + } + + /** A record from another uploader may have no `utcOffset` at all. Inbound that has to become 0, not null. */ + @Test + fun `a missing Nightscout offset becomes zero`() { + val ns = NSCarbs( + date = 10000, device = null, identifier = null, units = null, srvModified = null, srvCreated = null, + utcOffset = null, subject = null, isReadOnly = false, isValid = true, + eventType = app.aaps.core.nssdk.localmodel.treatment.EventType.CARBS_CORRECTION, + notes = null, pumpId = null, endId = null, pumpType = null, pumpSerial = null, + carbs = 1.0, duration = 0 + ) + + assertThat(ns.toCarbs().utcOffset).isEqualTo(0L) + } + + /** The helpers the converters are built from, checked directly. */ + @Test + fun `T converts between the two units`() { + assertThat(T.mins(plusTwoHoursMin).msecs()).isEqualTo(plusTwoHoursMs) + assertThat(T.msecs(plusTwoHoursMs).mins()).isEqualTo(plusTwoHoursMin) + } +} From f6a4e85e8a8c80385faa2ba4b12e24312ac575d1 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 10:32:54 +0200 Subject: [PATCH 005/146] :core:nssdk org.json -> kotlinx --- .../aaps/core/nssdk/NSAndroidClientImpl.kt | 32 +-- .../core/nssdk/interfaces/NSAndroidClient.kt | 20 +- .../nssdk/interfaces/RunningConfiguration.kt | 6 +- .../nssdk/networking/NetworkStackBuilder.kt | 21 +- .../networking/NightscoutRemoteService.kt | 25 ++- .../nssdk/remotemodel/RemoteProfileStore.kt | 49 ----- .../configBuilder/RunningConfigurationImpl.kt | 88 ++++---- .../RunningConfigurationImplTest.kt | 2 +- .../sync/nsclientV3/NSClientV3Plugin.kt | 15 +- .../clientcontrol/ClientControlPublisher.kt | 8 +- .../clientcontrol/ClientControlReceiver.kt | 33 +-- .../clientcontrol/ClientControlRoundTrip.kt | 33 +-- .../clientcontrol/PairingOfferFetcher.kt | 8 +- .../clientcontrol/PairingOfferPublisher.kt | 10 +- .../sync/nsclientV3/json/JsonBridge.kt | 32 +++ .../sync/nsclientV3/json/OrgJsonCompat.kt | 93 ++++++++ .../nsclientV3/services/NSClientV3Service.kt | 20 +- .../services/RunningConfigurationPublisher.kt | 47 ++-- .../workers/LoadProfileStoreWorker.kt | 10 +- .../nsclientV3/workers/LoadSettingsWorker.kt | 3 +- .../nsclientV3/JsonAccessorSemanticsTest.kt | 157 ++++++++++++++ .../ClientControlReceiverTest.kt | 41 ++-- .../ClientControlRoundTripTest.kt | 11 +- .../ClientControlUplinkIntegrationTest.kt | 6 +- .../clientcontrol/PairingOfferFetcherTest.kt | 20 +- .../sync/nsclientV3/json/OrgJsonCompatTest.kt | 204 ++++++++++++++++++ .../workers/LoadProfileStoreWorkerTest.kt | 35 +-- 27 files changed, 769 insertions(+), 260 deletions(-) delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/JsonBridge.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/JsonAccessorSemanticsTest.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt index af6a7cfd1223..5130a7b1c9ff 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt @@ -37,8 +37,9 @@ import com.google.gson.JsonParser import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import okhttp3.logging.HttpLoggingInterceptor -import org.json.JSONObject /** * @@ -500,9 +501,11 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) } - override suspend fun createProfileStore(remoteProfileStore: JSONObject): CreateUpdateResponse = callWrapper(dispatcher) { - remoteProfileStore.put("app", "AAPS") - val response = api.createProfile(JsonParser.parseString(remoteProfileStore.toString()).asJsonObject) + override suspend fun createProfileStore(remoteProfileStore: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { + // kotlinx JsonObject is immutable, so the app name goes into a copy instead of being put in + // place. Same result on the wire: an existing "app" key is replaced, a new one is added. + val stamped = JsonObject(remoteProfileStore + ("app" to JsonPrimitive("AAPS"))) + val response = api.createProfile(JsonParser.parseString(stamped.toString()).asJsonObject) if (response.isSuccessful) { if (response.code() == 200 || response.code() == 201) { return@callWrapper CreateUpdateResponse( @@ -523,7 +526,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) } - override suspend fun getLastProfileStore(): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { + override suspend fun getLastProfileStore(): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.getLastProfile() if (response.isSuccessful) { @@ -536,7 +539,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException("Unsuccessful") } - override suspend fun getProfileModifiedSince(from: Long): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { + override suspend fun getProfileModifiedSince(from: Long): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.getProfileModifiedSince(from) if (response.isSuccessful) { @@ -549,7 +552,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException("Unsuccessful") } - override suspend fun getSettings(identifier: String): NSAndroidClient.ReadResponse = callWrapper(dispatcher) { + override suspend fun getSettings(identifier: String): NSAndroidClient.ReadResponse = callWrapper(dispatcher) { val response = api.getSetting(identifier) if (response.isSuccessful) { @@ -568,7 +571,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException("Unsuccessful") } - override suspend fun getSettingsModifiedSince(from: Long, limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { + override suspend fun getSettingsModifiedSince(from: Long, limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.getSettingsModifiedSince(from, limit) if (response.isSuccessful) { @@ -585,7 +588,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException("Unsuccessful") } - override suspend fun searchSettings(limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { + override suspend fun searchSettings(limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.searchSettings(limit) if (response.isSuccessful) { @@ -602,10 +605,11 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException("Unsuccessful") } - override suspend fun createSettings(settings: JSONObject): CreateUpdateResponse = callWrapper(dispatcher) { + override suspend fun createSettings(settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { - settings.put("app", "AAPS") - val response = api.createSetting(JsonParser.parseString(settings.toString()).asJsonObject) + // See createProfileStore: kotlinx JsonObject is immutable, so stamp a copy. + val stamped = JsonObject(settings + ("app" to JsonPrimitive("AAPS"))) + val response = api.createSetting(JsonParser.parseString(stamped.toString()).asJsonObject) if (response.isSuccessful) { if (response.code() == 200 || response.code() == 201) { return@callWrapper CreateUpdateResponse( @@ -626,7 +630,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) } - override suspend fun patchSettings(identifier: String, settings: JSONObject): CreateUpdateResponse = callWrapper(dispatcher) { + override suspend fun patchSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { val response = api.patchSetting(JsonParser.parseString(settings.toString()).asJsonObject, identifier) if (response.code() == 404) { @@ -655,7 +659,7 @@ class NSAndroidClientImpl( throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) } - override suspend fun updateSettings(identifier: String, settings: JSONObject): CreateUpdateResponse = callWrapper(dispatcher) { + override suspend fun updateSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { val response = api.updateSetting(JsonParser.parseString(settings.toString()).asJsonObject, identifier) if (response.isSuccessful) { diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt index 1eeabb8d2e04..c40eaa5a4819 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt @@ -8,7 +8,7 @@ import app.aaps.core.nssdk.localmodel.food.NSFood import app.aaps.core.nssdk.localmodel.treatment.CreateUpdateResponse import app.aaps.core.nssdk.localmodel.treatment.NSTreatment import app.aaps.core.nssdk.remotemodel.LastModified -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface NSAndroidClient { @@ -40,9 +40,9 @@ interface NSAndroidClient { suspend fun createDeviceStatus(nsDeviceStatus: NSDeviceStatus): CreateUpdateResponse suspend fun getDeviceStatusModifiedSince(from: Long): List - suspend fun createProfileStore(remoteProfileStore: JSONObject): CreateUpdateResponse - suspend fun getProfileModifiedSince(from: Long): ReadResponse> - suspend fun getLastProfileStore(): ReadResponse> + suspend fun createProfileStore(remoteProfileStore: JsonObject): CreateUpdateResponse + suspend fun getProfileModifiedSince(from: Long): ReadResponse> + suspend fun getLastProfileStore(): ReadResponse> suspend fun createTreatment(nsTreatment: NSTreatment): CreateUpdateResponse suspend fun updateTreatment(nsTreatment: NSTreatment): CreateUpdateResponse @@ -52,18 +52,18 @@ interface NSAndroidClient { suspend fun createFood(nsFood: NSFood): CreateUpdateResponse suspend fun updateFood(nsFood: NSFood): CreateUpdateResponse - suspend fun getSettings(identifier: String): ReadResponse + suspend fun getSettings(identifier: String): ReadResponse /** History pull. Server requires `api:settings:admin` permission. */ - suspend fun getSettingsModifiedSince(from: Long, limit: Int = 100): ReadResponse> + suspend fun getSettingsModifiedSince(from: Long, limit: Int = 100): ReadResponse> /** List all settings docs. Server requires `api:settings:admin` permission. */ - suspend fun searchSettings(limit: Int = 100): ReadResponse> - suspend fun createSettings(settings: JSONObject): CreateUpdateResponse - suspend fun patchSettings(identifier: String, settings: JSONObject): CreateUpdateResponse + suspend fun searchSettings(limit: Int = 100): ReadResponse> + suspend fun createSettings(settings: JsonObject): CreateUpdateResponse + suspend fun patchSettings(identifier: String, settings: JsonObject): CreateUpdateResponse /** Upsert: replaces existing doc with [identifier], or inserts if absent. NS3 "UPDATE" semantics. */ - suspend fun updateSettings(identifier: String, settings: JSONObject): CreateUpdateResponse + suspend fun updateSettings(identifier: String, settings: JsonObject): CreateUpdateResponse suspend fun deleteSettings(identifier: String): CreateUpdateResponse /** Hard delete via NS `?permanent=true` — removes the doc instead of soft-deleting (tombstoning) it. */ diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt index c6ba3e20a177..29e50a4629db 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt @@ -1,16 +1,16 @@ package app.aaps.core.nssdk.interfaces import app.aaps.core.nssdk.localmodel.configuration.NSRunningConfiguration -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface RunningConfiguration { // called in AAPS mode only — "cold" settings doc: plugin config that changes only on user edits. - fun configuration(): JSONObject + fun configuration(): JsonObject // called in AAPS mode only — "hot" settings doc: runtime state that changes frequently // (active scene lifecycle) plus computed runtime flags. Published to a separate identifier. - fun activeSceneConfiguration(): JSONObject + fun activeSceneConfiguration(): JsonObject // called in NSClient mode only — apply the cold doc (everything except the active scene). fun applyCold(configuration: NSRunningConfiguration) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt index 32378507c223..ec9b5a35c171 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt @@ -4,10 +4,12 @@ import android.content.Context import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonDeserializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import org.json.JSONObject import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit @@ -95,12 +97,21 @@ internal object NetworkStackBuilder { return build() } - private val deserializer: JsonDeserializer = - JsonDeserializer { json, _, _ -> - JSONObject(json.asJsonObject.toString()) + /** + * Schema-less documents (profiles, settings) are carried as kotlinx [JsonObject] rather than + * being modelled, because AAPS does not own their shape. + * + * This used to build `org.json.JSONObject`, which is a JVM and Android API and cannot go to + * iOS. The bridge is the same as before - Gson tree to text, text to the target type - so the + * parsed result is unchanged. `asJsonObject` still throws for a non object, as it always did. + */ + private val deserializer: JsonDeserializer = + JsonDeserializer { json, _, _ -> + Json.parseToJsonElement(json.asJsonObject.toString()).jsonObject } + private fun provideGson(): Gson = GsonBuilder().also { - it.registerTypeAdapter(JSONObject::class.java, deserializer) + it.registerTypeAdapter(JsonObject::class.java, deserializer) }.create() private const val OK_HTTP_CACHE_SIZE = 10L * 1024 * 1024 diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt index 9f0389bd5c6d..c027a1d21fa7 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt @@ -1,5 +1,8 @@ package app.aaps.core.nssdk.networking +// Gson still builds the request bodies (the converter switch is a separate step), while responses +// already come back as kotlinx JsonObject. Both simple names are `JsonObject`, so the Gson one is +// aliased - when the converter goes, the alias and its uses go with it. import app.aaps.core.nssdk.remotemodel.LastModified import app.aaps.core.nssdk.remotemodel.NSResponse import app.aaps.core.nssdk.remotemodel.RemoteCreateUpdateResponse @@ -8,8 +11,7 @@ import app.aaps.core.nssdk.remotemodel.RemoteEntry import app.aaps.core.nssdk.remotemodel.RemoteFood import app.aaps.core.nssdk.remotemodel.RemoteStatusResponse import app.aaps.core.nssdk.remotemodel.RemoteTreatment -import com.google.gson.JsonObject -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE @@ -19,6 +21,7 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query +import com.google.gson.JsonObject as GsonJsonObject /** * Created by adrian on 2019-12-23. @@ -95,32 +98,32 @@ internal interface NightscoutRemoteService { suspend fun deleteFood(@Path("identifier") identifier: String): Response @GET("v3/profile/history/{from}") - suspend fun getProfileModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 10): Response>> + suspend fun getProfileModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 10): Response>> @GET("v3/profile?sort\$desc=date&limit=1") - suspend fun getLastProfile(): Response>> + suspend fun getLastProfile(): Response>> @POST("v3/profile") - suspend fun createProfile(@Body profile: JsonObject): Response + suspend fun createProfile(@Body profile: GsonJsonObject): Response @GET("v3/settings/{identifier}") - suspend fun getSetting(@Path("identifier") identifier: String): Response> + suspend fun getSetting(@Path("identifier") identifier: String): Response> @GET("v3/settings/history/{from}") - suspend fun getSettingsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 100): Response>> + suspend fun getSettingsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 100): Response>> @GET("v3/settings") - suspend fun searchSettings(@Query("limit") limit: Int = 100): Response>> + suspend fun searchSettings(@Query("limit") limit: Int = 100): Response>> @POST("v3/settings") - suspend fun createSetting(@Body settings: JsonObject): Response + suspend fun createSetting(@Body settings: GsonJsonObject): Response @PATCH("v3/settings/{identifier}") - suspend fun patchSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response + suspend fun patchSetting(@Body settings: GsonJsonObject, @Path("identifier") identifier: String): Response /** PUT (NS3 "UPDATE") = upsert. Replaces existing doc, or inserts if absent. */ @PUT("v3/settings/{identifier}") - suspend fun updateSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response + suspend fun updateSetting(@Body settings: GsonJsonObject, @Path("identifier") identifier: String): Response /** [permanent] = `null` → soft delete (tombstone); `true` → NS `?permanent=true` hard delete. */ @DELETE("v3/settings/{identifier}") diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt deleted file mode 100644 index 593d5f15f1fa..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteProfileStore.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.aaps.core.nssdk.remotemodel - -import com.google.gson.annotations.SerializedName -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable -import org.json.JSONObject - -/** - * DeviceStatus coming from uploader or AAPS - * - **/ -@Serializable -data class RemoteProfileStore( - @SerializedName("app") var app: String? = null, - @SerializedName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. - @SerializedName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. - @SerializedName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). - @SerializedName("created_at") val createdAt: String? = null, // string or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerializedName("date") val date: Long? = null, // date as milliseconds - @SerializedName("startDate") val startDate: Long? = null, // record valid from - @SerializedName("defaultProfile") val defaultProfile: String,// default profile in store - - //@Serializable(with = JSONSerializer::class) - @Contextual @SerializedName("store") val store: JSONObject -) { -/* - @Serializable data class Store( - val names: ArrayList, - val profiles: ArrayList - ) - - @Serializable data class SimpleProfile( - @SerializedName("dia") val dia: Double, - @SerializedName("carbratio") val carbratio: ArrayList, - @SerializedName("sens") val sens: ArrayList, - @SerializedName("basal") val basal: ArrayList, - @SerializedName("target_low") val target_low: ArrayList, - @SerializedName("target_high") val target_high: ArrayList, - @SerializedName("units") val units: String, // string The units for the glucose value, mg/dl or mmoll - @SerializedName("timezone") val timezone: String - ) - - @Serializable data class ProfileEntry( - @SerializedName("time") val time: String, - @SerializedName("timeAsSeconds") val timeAsSeconds: Long? = null, - @SerializedName("value") val value: Double - ) -*/ -} diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt index e4c4077f0ffb..cc9e6cf505ce 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt @@ -34,8 +34,9 @@ import app.aaps.core.nssdk.localmodel.configuration.NSRunningConfiguration import app.aaps.plugins.configuration.R import dagger.Reusable import kotlinx.serialization.json.Json -import org.json.JSONException -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import javax.inject.Inject @Reusable @@ -55,58 +56,51 @@ class RunningConfigurationImpl @Inject constructor( private val wireJson = Json { explicitNulls = false } // called in AAPS mode only - override fun configuration(): JSONObject { - val json = JSONObject() + override fun configuration(): JsonObject { // Before init completes no pump is selected yet, and activePump.isInitialized() would throw // "No pump selected" (PluginStore.activePumpInternal) instead of returning false. Treat // "app not initialized" as an earlier "pump not ready" state and return the empty doc — // RunningConfigurationPublisher skips an empty payload and retries once init completes. - if (!config.appInitialized) return json + if (!config.appInitialized) return JsonObject(emptyMap()) val pumpInterface = activePlugin.activePump - if (!pumpInterface.isInitialized()) return json - try { - // Plugin selection + settings + scene definitions all ride the generic key-sync path now - // (ActivePlugin* keys + the flat syncedPrefs cold block). - json.put("syncedPrefs", buildSyncedPrefs()) - json.put("pump", pumpInterface.model().description) + if (!pumpInterface.isInitialized()) return JsonObject(emptyMap()) + // Plugin selection + settings + scene definitions all ride the generic key-sync path now + // (ActivePlugin* keys + the flat syncedPrefs cold block). + return buildJsonObject { + put("syncedPrefs", buildSyncedPrefs()) + put("pump", pumpInterface.model().description) // Mirror the master's active-pump faking flag READ-ONLY to clients (so a follower's VirtualPump shows the // right EB capability + interprets emulated-temp EBs correctly) — computed here on the master, never on the client. // Reads activePump (the wrapper, which delegates to the real pump on the master); applyCold instead casts // activePumpInternal because a client only has VirtualPump (not the PumpWithConcentration wrapper). - json.put("isFakingTempsByExtendedBoluses", pumpInterface.isFakingTempsByExtendedBoluses) - json.put("version", config.VERSION_NAME) - } catch (e: JSONException) { - aapsLogger.error("Unhandled exception", e) + // Stays a real JSON boolean, as it was with org.json - everything under syncedPrefs is a string, this is not. + put("isFakingTempsByExtendedBoluses", pumpInterface.isFakingTempsByExtendedBoluses) + put("version", config.VERSION_NAME) } - return json } // called in AAPS mode only — the small "hot" doc: active scene (if any) + computed runtime flags. - override fun activeSceneConfiguration(): JSONObject { - val json = JSONObject() - try { - activeSceneSync.activeSceneSnapshot()?.let { snapshot -> - // Serialize from the wire DTO so the field set stays in lockstep with - // [NSActiveScene] (no hand-maintained put() list to drift). explicitNulls=false - // keeps the doc small by omitting not-yet-resolved NS ids. - val wire = NSActiveScene( - sceneId = snapshot.sceneId, - activatedAt = snapshot.activatedAt, - durationMs = snapshot.durationMs, - lifecycle = snapshot.lifecycle.name, - ttNsId = snapshot.ttNsId, - psNsId = snapshot.psNsId, - rmNsId = snapshot.rmNsId, - teNsId = snapshot.teNsId - ) - json.put("activeScene", JSONObject(wireJson.encodeToString(NSActiveScene.serializer(), wire))) - } - json.put("usedAutosensOnMainPhone", constraintsChecker.isAutosensModeEnabled().value()) - } catch (e: JSONException) { - aapsLogger.error("Unhandled exception", e) + override fun activeSceneConfiguration(): JsonObject = buildJsonObject { + activeSceneSync.activeSceneSnapshot()?.let { snapshot -> + // Serialize from the wire DTO so the field set stays in lockstep with + // [NSActiveScene] (no hand-maintained put() list to drift). explicitNulls=false + // keeps the doc small by omitting not-yet-resolved NS ids. + val wire = NSActiveScene( + sceneId = snapshot.sceneId, + activatedAt = snapshot.activatedAt, + durationMs = snapshot.durationMs, + lifecycle = snapshot.lifecycle.name, + ttNsId = snapshot.ttNsId, + psNsId = snapshot.psNsId, + rmNsId = snapshot.rmNsId, + teNsId = snapshot.teNsId + ) + // Straight to a JsonElement now - the old code went through text only because org.json + // could not read a kotlinx tree. + put("activeScene", wireJson.encodeToJsonElement(NSActiveScene.serializer(), wire)) } - return json + put("usedAutosensOnMainPhone", constraintsChecker.isAutosensModeEnabled().value()) } // All cold-synced values (plugin selection, plugin settings, scene/quick-wizard/automation/insulin @@ -131,19 +125,19 @@ class RunningConfigurationImpl @Inject constructor( // must travel and each device re-applies its own mode logic on read — hence NOT the full effective getter. // But a plain raw read would publish the literal default for computed-default keys (NsClientAllowClientControl, // AutosensPeriod, …), mis-telling clients the master's operative value; forSync uses the computed default instead. - private fun buildSyncedPrefs(): JSONObject { - val out = JSONObject() + // Every value here is written as a JSON *string*, including numbers and booleans. That is the + // existing wire format and the client parses it back that way, so it is kept as is. + private fun buildSyncedPrefs(): JsonObject = buildJsonObject { coldSyncKeys().forEach { key -> when (key) { - is BooleanNonPreferenceKey -> out.put(key.key, preferences.get(key, forSync = true).toString()) - is StringNonPreferenceKey -> out.put(key.key, preferences.get(key)) - is IntNonPreferenceKey -> out.put(key.key, preferences.get(key, forSync = true).toString()) - is DoubleNonPreferenceKey -> out.put(key.key, preferences.get(key).toString()) // no computed default → raw - is UnitDoublePreferenceKey -> out.put(key.key, preferences.getRaw(key).toString()) // raw mg/dl, 1:1 + is BooleanNonPreferenceKey -> put(key.key, preferences.get(key, forSync = true).toString()) + is StringNonPreferenceKey -> put(key.key, preferences.get(key)) + is IntNonPreferenceKey -> put(key.key, preferences.get(key, forSync = true).toString()) + is DoubleNonPreferenceKey -> put(key.key, preferences.get(key).toString()) // no computed default → raw + is UnitDoublePreferenceKey -> put(key.key, preferences.getRaw(key).toString()) // raw mg/dl, 1:1 else -> aapsLogger.warn(LTag.CORE, "syncedPrefs: unsupported key type for ${key.key}") } } - return out } // Apply master-published synced prefs on the client. "Master wins": adopt verbatim via putRemote diff --git a/plugins/configuration/src/test/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImplTest.kt b/plugins/configuration/src/test/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImplTest.kt index 129d104b6221..a267c03bd7f5 100644 --- a/plugins/configuration/src/test/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImplTest.kt +++ b/plugins/configuration/src/test/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImplTest.kt @@ -168,7 +168,7 @@ internal class RunningConfigurationImplTest { fun configurationBeforeAppInitializedReturnsEmptyWithoutTouchingPump() { whenever(config.appInitialized).thenReturn(false) val result = sut.configuration() - assertEquals(0, result.length()) + assertEquals(0, result.size) verify(activePlugin, never()).activePump } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 0b2c05efaa06..37de0da3e95b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -78,6 +78,7 @@ import app.aaps.plugins.sync.nsclientV3.extensions.toNSSvgV3 import app.aaps.plugins.sync.nsclientV3.extensions.toNSTemporaryBasal import app.aaps.plugins.sync.nsclientV3.extensions.toNSTemporaryTarget import app.aaps.plugins.sync.nsclientV3.extensions.toNSTherapyEvent +import app.aaps.plugins.sync.nsclientV3.json.JsonBridge.toKotlinxJson import app.aaps.plugins.sync.nsclientV3.keys.NsclientBooleanKey import app.aaps.plugins.sync.nsclientV3.keys.NsclientLongKey import app.aaps.plugins.sync.nsclientV3.keys.NsclientStringKey @@ -117,8 +118,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement -import org.json.JSONObject import java.security.InvalidParameterException import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -395,7 +396,7 @@ class NSClientV3Plugin @Inject constructor( * rejects cleanly with a signed ACK (one place, also covering the poll fallback) rather than silently * dropping it, which would time a client out into a false "master offline" alarm in the toggle race window. */ - fun handleClientControlSettingsEvent(identifier: String, doc: JSONObject) { + fun handleClientControlSettingsEvent(identifier: String, doc: JsonObject) { scope.launch { runCatching { clientControlReceiver.onSettingsDocChanged(identifier, doc) } .onFailure { aapsLogger.error(LTag.NSCLIENT, "ClientControl WS dispatch failed for $identifier: ${it.message}", it) } @@ -407,7 +408,7 @@ class NSClientV3Plugin @Inject constructor( * `aaps_clientcontrol_ack_` settings events here. Synchronous parse/verify/emit — the * round-trip coordinator only re-publishes to an in-process flow, no IO. */ - fun handleClientControlAckEvent(doc: JSONObject) { + fun handleClientControlAckEvent(doc: JsonObject) { runCatching { clientControlRoundTrip.onAckDoc(doc) } .onFailure { aapsLogger.error(LTag.NSCLIENT, "ClientControl ACK dispatch failed: ${it.message}", it) } } @@ -416,7 +417,7 @@ class NSClientV3Plugin @Inject constructor( * WS-push entry for a master→client bolus-progress frame (client side). NSClientV3Service routes * `aaps_clientcontrol_progress_` settings events here; feeds the client's own BolusProgressData. */ - fun handleClientControlProgressEvent(doc: JSONObject) { + fun handleClientControlProgressEvent(doc: JsonObject) { runCatching { clientControlRoundTrip.onProgressDoc(doc) } .onFailure { aapsLogger.error(LTag.NSCLIENT, "ClientControl progress dispatch failed: ${it.message}", it) } } @@ -757,7 +758,7 @@ class NSClientV3Plugin @Inject constructor( val data = (dataPair as DataSyncSelector.PairProfileStore).value try { nsClientRepository.addLog("► ADD $collection", "Sent ${dataPair.javaClass.simpleName} $progress", data) - nsAndroidClient?.createProfileStore(data)?.let { result -> + nsAndroidClient?.createProfileStore(data.toKotlinxJson())?.let { result -> when (result.response) { 200 -> nsClientRepository.addLog("◄ UPDATED", "OK ProfileStore") 201 -> nsClientRepository.addLog("◄ ADDED", "OK ProfileStore") @@ -836,6 +837,7 @@ class NSClientV3Plugin @Inject constructor( 201 -> nsClientRepository.addLog("◄ ADDED", "OK ${dataPair.value.javaClass.simpleName}") 400 -> nsClientRepository.addLog("◄ FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") + 404 -> { nsClientRepository.addLog("◄ NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") if (!config.isEnabled(ExternalOptions.IGNORE_NS_V3_ERRORS) && @@ -890,6 +892,7 @@ class NSClientV3Plugin @Inject constructor( 201 -> nsClientRepository.addLog("◄ ADDED", "OK ${dataPair.value.javaClass.simpleName}") 400 -> nsClientRepository.addLog("◄ FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") + 404 -> { nsClientRepository.addLog("◄ NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") if (!config.isEnabled(ExternalOptions.IGNORE_NS_V3_ERRORS) && @@ -944,6 +947,7 @@ class NSClientV3Plugin @Inject constructor( 201 -> nsClientRepository.addLog("◄ ADDED", "OK ${dataPair.value.javaClass.simpleName}") 400 -> nsClientRepository.addLog("◄ FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") + 404 -> { nsClientRepository.addLog("◄ NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") if (!config.isEnabled(ExternalOptions.IGNORE_NS_V3_ERRORS) && @@ -1019,6 +1023,7 @@ class NSClientV3Plugin @Inject constructor( 201 -> nsClientRepository.addLog("◄ ADDED", "OK ${dataPair.value.javaClass.simpleName}") 400 -> nsClientRepository.addLog("◄ FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") + 404 -> { nsClientRepository.addLog("◄ NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}") if (!config.isEnabled(ExternalOptions.IGNORE_NS_V3_ERRORS) && diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlPublisher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlPublisher.kt index 4467b899cc0f..9f54b765bf76 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlPublisher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlPublisher.kt @@ -9,9 +9,10 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.ClientControlMessage import app.aaps.core.nssdk.localmodel.clientcontrol.SignedEnvelope import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import org.json.JSONObject +import kotlinx.serialization.json.put import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -151,8 +152,7 @@ class ClientControlPublisher @Inject constructor( aapsLogger.error(LTag.NSCLIENT, "ClientControl: NS client not initialized") return ClientControlSendResult.PublishFailed("NS client not initialized") } - val envelopeJson = json.encodeToString(SignedEnvelope.serializer(), envelope) - val doc = JSONObject().apply { + val doc = buildJsonObject { // validateCommon requires date/utcOffset/app, but `date` is immutable after first // create — sending the live envelope timestamp here fails the second PUT to the same // identifier with HTTP 400. The authoritative timestamp is envelope.timestamp (signed); @@ -161,7 +161,7 @@ class ClientControlPublisher @Inject constructor( put("utcOffset", 0) put("app", "AAPS") put("schemaVersion", SCHEMA_VERSION) - put("envelope", JSONObject(envelopeJson)) + put("envelope", json.encodeToJsonElement(SignedEnvelope.serializer(), envelope)) } return runCatching { client.updateSettings(identifier, doc) } .fold( diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt index 014c1edae2cf..b69b1bded3de 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt @@ -4,7 +4,6 @@ import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.bolus.WizardBolusExecutor import app.aaps.core.interfaces.clientcontrol.FailureReason -import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.di.ApplicationScope @@ -17,10 +16,10 @@ import app.aaps.core.interfaces.nsclient.NSClientRepository import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.queue.CommandQueue +import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.scenes.SceneAutomationApi import app.aaps.core.interfaces.scenes.SceneAutomationResult import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.LongComposedKey import app.aaps.core.keys.interfaces.BooleanNonPreferenceKey import app.aaps.core.keys.interfaces.DoubleNonPreferenceKey @@ -38,12 +37,14 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.BolusPreview import app.aaps.core.nssdk.localmodel.clientcontrol.ClientControlMessage import app.aaps.core.nssdk.localmodel.clientcontrol.ClientState import app.aaps.core.nssdk.localmodel.clientcontrol.ConfirmationLineDto -import app.aaps.core.nssdk.localmodel.clientcontrol.WizardDetailDto import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressPhase import app.aaps.core.nssdk.localmodel.clientcontrol.SignedEnvelope +import app.aaps.core.nssdk.localmodel.clientcontrol.WizardDetailDto import app.aaps.core.nssdk.utils.ClientControlCrypto import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat import app.aaps.plugins.sync.nsclientV3.services.RunningConfigurationPublisher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -52,7 +53,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.Json -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -132,12 +135,14 @@ class ClientControlReceiver @Inject constructor( private val progressClientId = AtomicReference(null) @Volatile private var progressArmedAt = 0L @Volatile private var progressDelivering = false + // Generation of bolusProgressData captured at arm time. Only a bolus that start()s a NEWER generation (the // client's own, queued AFTER this commit) is mirrored — so a bolus already running on the master when the // client commits (the client's was queue-rejected) is never mis-attributed to the client's progress dialog. @Volatile private var progressArmedGeneration = 0L private val progressSampleMs = 1_000L private val progressArmTtlMs = 60_000L + // Liveness heartbeat: some pump drivers (Medtronic, Omnipod Eros) deliver a bolus as one blocking call and // emit NO intermediate progress. Re-publishing the current frame on this cadence keeps the client's stall // watchdog from false-firing on a healthy long bolus; only a real relay/connection outage produces silence. @@ -239,7 +244,7 @@ class ClientControlReceiver @Inject constructor( val now = dateUtil.now() val resp = runCatching { client.searchSettings(limit = 100) }.getOrNull() ?: return for (doc in resp.values) { - val identifier = doc.optString("identifier") + val identifier = doc.optStringCompat("identifier") if (!identifier.startsWith(ClientControlPublisher.IDENTIFIER_PREFIX)) continue // Skip our own pairing offers — they share the prefix but are not signed envelopes, // so verifyAndAck would (harmlessly) log them as "malformed envelope, ignoring". We @@ -257,7 +262,7 @@ class ClientControlReceiver @Inject constructor( * WS-push entry. NSClientV3Service routes settings-collection create/update events * to here for any identifier with the [ClientControlPublisher.IDENTIFIER_PREFIX]. */ - suspend fun onSettingsDocChanged(identifier: String, doc: JSONObject) { + suspend fun onSettingsDocChanged(identifier: String, doc: JsonObject) { if (!identifier.startsWith(ClientControlPublisher.IDENTIFIER_PREFIX)) return if (identifier.startsWith(ClientControlPublisher.IDENTIFIER_OFFER_PREFIX)) return // Command ACKs are master-written, consumed by clients — never an inbound command here. @@ -267,7 +272,7 @@ class ClientControlReceiver @Inject constructor( verifyAndAck(identifier, doc, dateUtil.now()) } - private suspend fun verifyAndAck(identifier: String, doc: JSONObject, now: Long): Unit = commandMutex.withLock { + private suspend fun verifyAndAck(identifier: String, doc: JsonObject, now: Long): Unit = commandMutex.withLock { val client = nsClientV3Plugin.get().nsAndroidClient ?: return // Unrecognized / unverifiable docs are IGNORED, never deleted. A delete soft-deletes // (tombstones) the identifier on NS, so the next legitimate PUT to that same per-type slot @@ -275,11 +280,13 @@ class ClientControlReceiver @Inject constructor( // master that doesn't have this client paired) the WS echo makes one instance delete a // command another instance legitimately owns. Replay is already prevented by the per-client // counter; stray/garbage docs simply linger until NS auto-prunes them. - val envelopeObj = doc.optJSONObject("envelope") ?: run { + val envelopeObj = doc.optJsonObjectCompat("envelope") ?: run { aapsLogger.error(LTag.NSCLIENT, "ClientControl: $identifier has no envelope field, ignoring") return } - val envelope = runCatching { json.decodeFromString(envelopeObj.toString()) }.getOrNull() + // Decoded straight from the tree - the text round trip only existed because org.json and + // kotlinx could not share one. + val envelope = runCatching { json.decodeFromJsonElement(SignedEnvelope.serializer(), envelopeObj) }.getOrNull() if (envelope == null) { aapsLogger.error(LTag.NSCLIENT, "ClientControl: $identifier malformed envelope, ignoring") return @@ -788,12 +795,12 @@ class ClientControlReceiver @Inject constructor( AckEnvelope(clientId = clientId, commandCounter = commandCounter, phase = phase, status = status, reason = reason, payload = payload, timestamp = now, signature = "") ) val identifier = ClientControlPublisher.IDENTIFIER_ACK_PREFIX + clientId - val doc = JSONObject().apply { + val doc = buildJsonObject { put("date", ClientControlPublisher.DOC_DATE) put("utcOffset", 0) put("app", "AAPS") put("schemaVersion", ClientControlPublisher.SCHEMA_VERSION) - put("ack", JSONObject(json.encodeToString(AckEnvelope.serializer(), ack))) + put("ack", json.encodeToJsonElement(AckEnvelope.serializer(), ack)) } runCatching { client.updateSettings(identifier, doc) } .onSuccess { nsClientRepository.addLog("► CLIENTCTL", "ack $phase/$status counter=$commandCounter" + (reason?.let { " ($it)" } ?: "")) } @@ -828,12 +835,12 @@ class ClientControlReceiver @Inject constructor( ) ) val identifier = ClientControlPublisher.IDENTIFIER_PROGRESS_PREFIX + clientId - val doc = JSONObject().apply { + val doc = buildJsonObject { put("date", ClientControlPublisher.DOC_DATE) put("utcOffset", 0) put("app", "AAPS") put("schemaVersion", ClientControlPublisher.SCHEMA_VERSION) - put("progress", JSONObject(json.encodeToString(ProgressEnvelope.serializer(), env))) + put("progress", json.encodeToJsonElement(ProgressEnvelope.serializer(), env)) } runCatching { client.updateSettings(identifier, doc) } .onFailure { aapsLogger.error(LTag.NSCLIENT, "ClientControl: progress write failed for $identifier: ${it.message}") } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt index 80f54a278fcb..82db9d5245cb 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt @@ -6,7 +6,6 @@ import app.aaps.core.interfaces.clientcontrol.ActionProgress import app.aaps.core.interfaces.clientcontrol.ClientControlActionDispatcher import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.clientcontrol.PendingAction -import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger @@ -17,6 +16,7 @@ import app.aaps.core.interfaces.nsclient.NSClientRepository import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.scenes.ClientControlSendResult import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.nssdk.localmodel.clientcontrol.AckEnvelope @@ -25,13 +25,15 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.AckStatus import app.aaps.core.nssdk.localmodel.clientcontrol.BolusPreview import app.aaps.core.nssdk.localmodel.clientcontrol.ClientControlMessage import app.aaps.core.nssdk.localmodel.clientcontrol.PrefEntry -import app.aaps.core.nssdk.localmodel.clientcontrol.WizardDetailDto import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressPhase +import app.aaps.core.nssdk.localmodel.clientcontrol.WizardDetailDto import app.aaps.core.nssdk.utils.ClientControlCrypto import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin +import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.PROGRESS_WATCHDOG_MS import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.PROPAGATION_MARGIN_MS import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.ROUND_TRIP_TTL_MS +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -49,7 +51,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -150,9 +152,9 @@ class ClientControlRoundTrip @Inject constructor( * addressed to this client, verifies the signature against the paired master secret (a forged * "Ok" must not surface as Applied), and republishes to the in-process [ackEvents]. */ - fun onAckDoc(doc: JSONObject) { - val ackObj = doc.optJSONObject("ack") ?: return - val ack = runCatching { json.decodeFromString(ackObj.toString()) }.getOrNull() ?: run { + fun onAckDoc(doc: JsonObject) { + val ackObj = doc.optJsonObjectCompat("ack") ?: return + val ack = runCatching { json.decodeFromJsonElement(AckEnvelope.serializer(), ackObj) }.getOrNull() ?: run { aapsLogger.error(LTag.NSCLIENT, "ClientControl: malformed ACK doc") return } @@ -189,9 +191,9 @@ class ClientControlRoundTrip @Inject constructor( * must not be believable), drops a late/out-of-order frame, then drives the client's OWN [BolusProgressData] * so the existing (un-gated) progress dialog lights up — no client-specific UI. */ - fun onProgressDoc(doc: JSONObject) { - val obj = doc.optJSONObject("progress") ?: return - val env = runCatching { json.decodeFromString(obj.toString()) }.getOrNull() ?: run { + fun onProgressDoc(doc: JsonObject) { + val obj = doc.optJsonObjectCompat("progress") ?: return + val env = runCatching { json.decodeFromJsonElement(ProgressEnvelope.serializer(), obj) }.getOrNull() ?: run { aapsLogger.error(LTag.NSCLIENT, "ClientControl: malformed progress doc") return } @@ -213,8 +215,13 @@ class ClientControlRoundTrip @Inject constructor( armProgressWatchdog() } - ProgressPhase.Complete -> { cancelProgressWatchdog(); bolusProgressData.completeAndAutoClear() } - ProgressPhase.Cleared -> { cancelProgressWatchdog(); bolusProgressData.clear() } + ProgressPhase.Complete -> { + cancelProgressWatchdog(); bolusProgressData.completeAndAutoClear() + } + + ProgressPhase.Cleared -> { + cancelProgressWatchdog(); bolusProgressData.clear() + } } } @@ -425,8 +432,8 @@ class ClientControlRoundTrip @Inject constructor( val clientId = pairingRepository.currentPairing()?.clientId ?: return null val identifier = ClientControlPublisher.IDENTIFIER_ACK_PREFIX + clientId val doc = runCatching { client.getSettings(identifier) }.getOrNull()?.values ?: return null - val ackObj = doc.optJSONObject("ack") ?: return null - val ack = runCatching { json.decodeFromString(ackObj.toString()) }.getOrNull() ?: return null + val ackObj = doc.optJsonObjectCompat("ack") ?: return null + val ack = runCatching { json.decodeFromJsonElement(AckEnvelope.serializer(), ackObj) }.getOrNull() ?: return null if (ack.clientId != clientId || ack.commandCounter != counter || ack.phase != AckPhase.Done) return null val secret = pairingRepository.secretBytesOrNull() ?: return null if (!ClientControlCrypto.verifyAck(secret, ack)) return null diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt index 222238fd19b0..ba3b14ab5b7e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt @@ -9,6 +9,8 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.PairingOffer import app.aaps.core.nssdk.localmodel.clientcontrol.PairingPayload import app.aaps.core.nssdk.utils.ClientControlPairingCrypto import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -77,12 +79,12 @@ class PairingOfferFetcher @Inject constructor( var candidates = 0 val matches = mutableListOf() for (doc in resp.values) { - val identifier = doc.optString("identifier") + val identifier = doc.optStringCompat("identifier") if (!identifier.startsWith(ClientControlPublisher.IDENTIFIER_OFFER_PREFIX)) continue scanned++ - val offerObj = doc.optJSONObject("offer") ?: continue + val offerObj = doc.optJsonObjectCompat("offer") ?: continue val offer = try { - json.decodeFromString(offerObj.toString()) + json.decodeFromJsonElement(PairingOffer.serializer(), offerObj) } catch (_: SerializationException) { aapsLogger.warn(LTag.NSCLIENT, "PairingOfferFetcher: offer $identifier malformed, skipping") continue diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferPublisher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferPublisher.kt index 13e5d945e3ac..cf08bc803beb 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferPublisher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferPublisher.kt @@ -11,7 +11,9 @@ import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import java.io.IOException import javax.inject.Inject import javax.inject.Provider @@ -93,7 +95,7 @@ class PairingOfferPublisher @Inject constructor( } } - private fun buildOfferDoc(payload: PairingPayload, pin: String): JSONObject { + private fun buildOfferDoc(payload: PairingPayload, pin: String): JsonObject { val payloadBytes = json.encodeToString(PairingPayload.serializer(), payload).toByteArray(Charsets.UTF_8) val salt = ClientControlPairingCrypto.newSalt() val iv = ClientControlPairingCrypto.newIv() @@ -105,14 +107,14 @@ class PairingOfferPublisher @Inject constructor( ivB64 = Base64.encodeToString(iv, Base64.NO_WRAP), wrappedB64 = Base64.encodeToString(wrapped, Base64.NO_WRAP) ) - return JSONObject().apply { + return buildJsonObject { // Same placeholder convention as ClientControlPublisher.uploadEnvelope — `date` is // immutable on subsequent PUTs, so a constant placeholder is the only safe value. put("date", ClientControlPublisher.DOC_DATE) put("utcOffset", 0) put("app", "AAPS") put("schemaVersion", ClientControlPublisher.SCHEMA_VERSION) - put("offer", JSONObject(json.encodeToString(PairingOffer.serializer(), offer))) + put("offer", json.encodeToJsonElement(PairingOffer.serializer(), offer)) } } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/JsonBridge.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/JsonBridge.kt new file mode 100644 index 000000000000..e388e6c0c512 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/JsonBridge.kt @@ -0,0 +1,32 @@ +package app.aaps.plugins.sync.nsclientV3.json + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.json.JSONObject + +/** + * Conversions between `org.json` and kotlinx JSON, for the places where the two still meet. + * + * `:core:nssdk` no longer speaks `org.json` - it cannot, because `org.json` is a JVM and Android API + * and the module has to build for iOS. Two things on the Android side still do, and neither is worth + * changing yet: + * + * 1. **socket.io.** `io.socket:socket.io-client` hands every event payload over as + * `org.json.JSONObject`. That is the library's API, so the conversion happens as the event + * arrives and everything downstream is kotlinx. + * 2. **The profile subsystem.** `ProfileStore`, `PureProfile` and `DataSyncSelector.PairProfileStore` + * are built on `org.json` throughout, far outside this module. Profiles are converted where they + * cross into or out of the client instead of migrating that subsystem here. + * + * Both conversions go through text, which is exactly what the old code did internally, so nothing + * about the parsed result changes. They are not free - do not put them on a hot path. + */ +object JsonBridge { + + /** `org.json` tree -> kotlinx tree. Throws if the text is somehow not a JSON object. */ + fun JSONObject.toKotlinxJson(): JsonObject = Json.parseToJsonElement(toString()).jsonObject + + /** kotlinx tree -> `org.json` tree. */ + fun JsonObject.toOrgJson(): JSONObject = JSONObject(toString()) +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt new file mode 100644 index 000000000000..24407ba9a794 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt @@ -0,0 +1,93 @@ +package app.aaps.plugins.sync.nsclientV3.json + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull + +/** + * Accessors on kotlinx [JsonObject] that behave exactly like the `org.json` ones they replace. + * + * `org.json` is a JVM and Android API, so it cannot go to iOS. The NSClientV3 wire layer is moving + * to kotlinx [JsonObject], but the two libraries disagree about a missing key and nothing about that + * disagreement shows up at compile time: + * + * ``` + * doc.optString("k") // org.json : "" when absent, never null + * doc["k"]?.jsonPrimitive?.content // kotlinx : null when absent + * ``` + * + * A straight translation would silently flip every downstream `isEmpty()` check and `?:` fallback. + * These functions keep the old behaviour so the type swap changes nothing that can be observed, + * and `OrgJsonCompatTest` checks each one against the real `org.json` over a shared matrix of + * inputs. + * + * **This is a migration shim, not a design.** It deliberately copies quirks that are probably bugs - + * see [optString] returning the four letter text `null`. They are kept so the move off `org.json` + * is provably behaviour preserving. Fixing them is a separate, visible change. + * + * It lives in `:plugins:sync` on purpose: this module is Android only (WorkManager, socket.io + * Service) and will never build for iOS, so the `org.json` quirks stay out of the shared modules. + */ +object OrgJsonCompat { + + /** + * Same as `org.json.JSONObject.optString(name)`. + * + * - missing key -> `""` + * - explicit JSON null -> the four letter text `"null"`, because Android's `org.json` renders + * its `JSONObject.NULL` sentinel through `String.valueOf` + * - string -> the string itself, unquoted + * - number or boolean -> its text form + * - object or array -> its JSON text + * + * Never returns Kotlin null. Callers written against `org.json` rely on that, sometimes without + * meaning to: `NSClientV3Service.onDataDelete` guards with `?: return@Listener`, which can never + * fire today. + */ + fun JsonObject.optStringCompat(key: String): String { + val element = this[key] ?: return "" + return when (element) { + is JsonNull -> "null" + is JsonPrimitive -> element.content + else -> element.toString() + } + } + + /** + * Same as `org.json.JSONObject.optJSONObject(name)`: null when the key is missing, when the + * value is an explicit JSON null, or when it holds anything that is not an object. + */ + fun JsonObject.optJsonObjectCompat(key: String): JsonObject? = this[key] as? JsonObject + + /** + * Same as `org.json.JSONObject.optLong(name, fallback)`. + * + * Coerces like `org.json` does: a numeric string is parsed, and a floating point value is + * truncated toward zero. Anything that cannot be read as a number gives [fallback]. + */ + fun JsonObject.optLongCompat(key: String, fallback: Long): Long { + val primitive = this[key] as? JsonPrimitive ?: return fallback + if (primitive is JsonNull) return fallback + primitive.longOrNull?.let { return it } + primitive.doubleOrNull?.let { return it.toLong() } + return fallback + } + + /** + * Same as `org.json.JSONObject.optBoolean(name)`: false unless the value is a real `true`, or + * the text `"true"` in any case. `org.json` accepts the string form too. + */ + fun JsonObject.optBooleanCompat(key: String): Boolean { + val primitive = this[key] as? JsonPrimitive ?: return false + if (primitive is JsonNull) return false + primitive.booleanOrNull?.let { return it } + return primitive.content.equals("true", ignoreCase = true) + } + + /** Same as `org.json.JSONObject.optJSONArray(name)`. */ + fun JsonObject.optJsonArrayCompat(key: String): JsonArray? = this[key] as? JsonArray +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index 77d92edca63f..e9d30f753fee 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -37,6 +37,7 @@ import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlPublisher import app.aaps.plugins.sync.nsclientV3.clientcontrol.OrphanDetector import app.aaps.plugins.sync.nsclientV3.data.NSDeviceStatusHandler import app.aaps.plugins.sync.nsclientV3.extensions.toRunningConfiguration +import app.aaps.plugins.sync.nsclientV3.json.JsonBridge.toKotlinxJson import app.aaps.plugins.sync.nsclientV3.keys.NsclientBooleanKey import dagger.android.DaggerService import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -299,6 +300,9 @@ class NSClientV3Service : DaggerService() { "settings" -> { val identifier = docJson.optString("identifier") + // socket.io hands every payload over as org.json, while the client control handlers + // speak the nssdk's kotlinx type. The conversion sits on each branch below, not up + // here: only one branch runs per event, and the cold/state branches never need it. when { // Client-side: cold config doc — apply everything except the active scene. config.AAPSCLIENT && identifier == SettingsIdentifiers.COLD -> @@ -319,17 +323,17 @@ class NSClientV3Service : DaggerService() { // IDENTIFIER_PREFIX branch (ack identifiers share that prefix) so the master // receiver never tries to verify an ack as an inbound command envelope. config.AAPSCLIENT && identifier.startsWith(ClientControlPublisher.IDENTIFIER_ACK_PREFIX) -> - nsClientV3Plugin.handleClientControlAckEvent(docJson) + nsClientV3Plugin.handleClientControlAckEvent(docJson.toKotlinxJson()) // Client-side: master→client live bolus-progress mirror. Same ordering rule as ACK (shares // IDENTIFIER_PREFIX) so the master never treats its own progress doc as an inbound command. config.AAPSCLIENT && identifier.startsWith(ClientControlPublisher.IDENTIFIER_PROGRESS_PREFIX) -> - nsClientV3Plugin.handleClientControlProgressEvent(docJson) + nsClientV3Plugin.handleClientControlProgressEvent(docJson.toKotlinxJson()) // Master-side: route client-control envelopes (paired-client → master commands) // to the receiver. The plugin gates on the master toggle internally. // !config.AAPSCLIENT: NS WS echoes every write back to the sender too — a client must not // self-process its own outgoing commands (unknown clientId → deleteSettings → HTTP 410 tombstone). - !config.AAPSCLIENT && identifier.startsWith(ClientControlPublisher.IDENTIFIER_PREFIX) -> - nsClientV3Plugin.handleClientControlSettingsEvent(identifier, docJson) + !config.AAPSCLIENT && identifier.startsWith(ClientControlPublisher.IDENTIFIER_PREFIX) -> + nsClientV3Plugin.handleClientControlSettingsEvent(identifier, docJson.toKotlinxJson()) } } } @@ -338,8 +342,12 @@ class NSClientV3Service : DaggerService() { private val onDataDelete = Emitter.Listener { args -> val response = args[0] as JSONObject aapsLogger.debug(LTag.NSCLIENT, "onDataDelete: $response") - val collection = response.optString("colName") ?: return@Listener - val identifier = response.optString("identifier") ?: return@Listener + // No elvis here: optString never returns null, it returns "" for a missing key. The old + // `?: return@Listener` looked like a guard but could not fire - Kotlin allowed it only + // because org.json is Java and the type is the platform type String!. A doc with no colName + // simply matches none of the collection checks below, exactly as it did before. + val collection = response.optString("colName") + val identifier = response.optString("identifier") nsClientRepository.addLog("◄ WS DELETE", "$collection $identifier") if (collection == "treatments") { storeDataForDb.addToDeleteTreatment(identifier) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt index 72954d23f07c..3772f303ccd5 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt @@ -22,8 +22,8 @@ import app.aaps.plugins.sync.nsclientV3.services.RunningConfigurationPublisher.C import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.debounce @@ -31,8 +31,11 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import org.json.JSONArray -import org.json.JSONObject +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -157,23 +160,35 @@ class RunningConfigurationPublisher @Inject constructor( // Cold doc: full plugin/overview/definition config + the authorized-clients roster. // Returns true if a doc was published, false if skipped because the pump is not initialized yet. private suspend fun publishCold(): Boolean { - val payload = runningConfiguration.configuration() - if (payload.length() == 0) return false // pump not initialized yet - // Append the authorized-clients roster directly on the runningConfig JSONObject — the + val base = runningConfiguration.configuration() + if (base.isEmpty()) return false // pump not initialized yet + // Append the authorized-clients roster directly on the runningConfig doc — the // canonical RunningConfiguration plugin (in :plugins:configuration) cannot reach // AuthorizedClientsRepository without a new inter-module dependency, so we attach // this master-local block here instead. Only Active entries are exposed. val activeClientIds = authorizedRepository.current(dateUtil.now()) .filter { it.state == ClientState.Active } .map { it.clientId } - payload.put("authorizedClients", JSONObject().put("clientIds", JSONArray(activeClientIds))) - // Advertise whether commands can actually be served, not just whether the user allows them. - // With the WebSocket off this master sees a command minutes late, past its validity, so it must - // not tell clients it is accepting: they fold this value into masterReachable and would keep - // offering edit UI for commands that can only expire. Only the PUBLISHED value is masked — the - // user's stored switch is untouched, and turning the WebSocket back on republishes it as true. - payload.optJSONObject(SYNCED_PREFS) - ?.put(BooleanKey.NsClientAllowClientControl.key, preferences.clientControlOperational().toString()) + // kotlinx JsonObject is immutable, so the doc is rebuilt instead of edited in place. Same + // result: the original keys in their original order, syncedPrefs carrying the masked value, + // and authorizedClients appended last. + val payload = buildJsonObject { + base.forEach { (key, value) -> + // Advertise whether commands can actually be served, not just whether the user allows them. + // With the WebSocket off this master sees a command minutes late, past its validity, so it must + // not tell clients it is accepting: they fold this value into masterReachable and would keep + // offering edit UI for commands that can only expire. Only the PUBLISHED value is masked — the + // user's stored switch is untouched, and turning the WebSocket back on republishes it as true. + if (key == SYNCED_PREFS && value is JsonObject) + put( + key, + JsonObject(value + (BooleanKey.NsClientAllowClientControl.key to JsonPrimitive(preferences.clientControlOperational().toString()))) + ) + else + put(key, value) + } + put("authorizedClients", buildJsonObject { put("clientIds", JsonArray(activeClientIds.map { JsonPrimitive(it) })) }) + } putSettings(SettingsIdentifiers.COLD, payload) return true } @@ -183,9 +198,9 @@ class RunningConfigurationPublisher @Inject constructor( putSettings(SettingsIdentifiers.STATE, runningConfiguration.activeSceneConfiguration()) } - private suspend fun putSettings(identifier: String, runningConfig: JSONObject) { + private suspend fun putSettings(identifier: String, runningConfig: JsonObject) { val client = nsClientV3Plugin.get().nsAndroidClient ?: return - val doc = JSONObject().apply { + val doc = buildJsonObject { // NS APIv3 validateCommon requires date / utcOffset / app on UPDATE; all three are // immutable after first create, so pick stable constants — the doc represents a // configuration snapshot, not an event in time. diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt index 497e9f4441c6..ccbf2538460f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt @@ -15,10 +15,11 @@ import app.aaps.core.objects.workflow.LoggingWorker import app.aaps.core.utils.JsonHelper import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.NsIncomingDataProcessor +import app.aaps.plugins.sync.nsclientV3.json.JsonBridge.toOrgJson import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import kotlin.math.max @HiltWorker @@ -40,10 +41,13 @@ class LoadProfileStoreWorker @AssistedInject constructor( val isFirstLoad = nsClientV3Plugin.isFirstLoad(NsClient.Collection.PROFILE) val lastLoaded = max(nsClientV3Plugin.lastLoadedSrvModified.collections.profile, dateUtil.now() - nsClientV3Plugin.maxAge) if ((nsClientV3Plugin.newestDataOnServer?.collections?.profile ?: Long.MAX_VALUE) > lastLoaded) { - val response: NSAndroidClient.ReadResponse> = + val response: NSAndroidClient.ReadResponse> = if (isFirstLoad) nsAndroidClient.getLastProfileStore() else nsAndroidClient.getProfileModifiedSince(lastLoaded) - val profiles = response.values + // The profile subsystem (ProfileStore, PureProfile, JsonHelper) is built on org.json + // well outside this module, so profiles are converted back here at the boundary + // rather than migrating all of it. See JsonBridge. + val profiles = response.values.map { it.toOrgJson() } if (profiles.isNotEmpty()) { val profile = profiles[profiles.size - 1] // if srvModified found in response diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt index 7f5dc3b307e8..5a1afe247795 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt @@ -18,6 +18,7 @@ import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.SettingsIdentifiers import app.aaps.plugins.sync.nsclientV3.clientcontrol.OrphanDetector import app.aaps.plugins.sync.nsclientV3.extensions.toRunningConfiguration +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optLongCompat import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers @@ -89,7 +90,7 @@ class LoadSettingsWorker @AssistedInject constructor( return } // ETag isn't set for settings GETs, but srvModified is in the doc body - val srvModified = doc.optLong("srvModified", 0L).takeIf { it > 0 } ?: response.lastServerModified + val srvModified = doc.optLongCompat("srvModified", 0L).takeIf { it > 0 } ?: response.lastServerModified doc.toString().toRunningConfiguration()?.let { configuration -> apply(configuration, srvModified) val ts = srvModified?.let { dateUtil.dateAndTimeAndSecondsString(it) } ?: "?" diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/JsonAccessorSemanticsTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/JsonAccessorSemanticsTest.kt new file mode 100644 index 000000000000..5d7ddca02f9b --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/JsonAccessorSemanticsTest.kt @@ -0,0 +1,157 @@ +package app.aaps.plugins.sync.nsclientV3 + +import com.google.common.truth.Truth.assertThat +import org.json.JSONObject +import org.junit.jupiter.api.Test + +/** + * Pins the exact semantics of the `org.json` accessors the NSClientV3 code relies on. + * + * The wire layer is moving off `org.json.JSONObject` to `kotlinx.serialization.json.JsonObject`, + * because `org.json` is a JVM/Android API and cannot go to iOS. The two libraries do **not** behave + * the same for a missing key, and nothing about that difference shows up at compile time: + * + * ``` + * doc.optString("k") // org.json : "" when absent, never null + * doc["k"]?.jsonPrimitive?.content // kotlinx : null when absent + * ``` + * + * So a straight translation silently flips every downstream `isEmpty()` check and `?:` fallback. + * These tests record what the current code actually gets, so the replacement can be checked against + * it instead of against an assumption. + * + * They also confirm **which** `org.json` is on the unit test classpath. The module sets + * `isReturnDefaultValues = true`, which would make the `android.jar` stub return null/0/false from + * every method - that would make every JSONObject-based test in this module meaningless. The real + * implementation arrives transitively through `io.socket:socket.io-client`. If these tests ever + * start failing with nulls and zeroes, that transitive dependency is gone and a real `org.json:json` + * test dependency is needed. + */ +class JsonAccessorSemanticsTest { + + // ---------------------------------------------------------------- optString + + /** + * The important one. `optString` returns a **non-null** `String`, so a missing key gives `""`. + * + * This is what makes `NSClientV3Service.onDataDelete` read + * `response.optString("colName") ?: return@Listener` - Kotlin sees the Java platform type + * `String!` and allows the elvis without a warning, but it can never fire. See + * `deadElvisOnOptString` below. + */ + @Test + fun `optString returns empty string for a missing key, not null`() { + val doc = JSONObject("""{"present":"value"}""") + + assertThat(doc.optString("present")).isEqualTo("value") + assertThat(doc.optString("missing")).isEqualTo("") + assertThat(doc.optString("missing")).isNotNull() + } + + /** + * An explicit JSON null is **not** the same as a missing key, and it is not Kotlin null either - + * it comes back as the four letter text `null`. + * + * This is a landmine for the migration. `NSClientV3Service` logs + * `data.optString("message")` straight into the NSClient log, so `{"message":null}` from the + * server currently writes the literal word "null" into the user visible log. kotlinx would give + * Kotlin null there instead. + * + * The two org.json implementations disagree here: this is the behaviour of the one Android + * ships (`JSON.toString(JSONObject.NULL)` -> `"null"`). Crockford's reference implementation + * returns the fallback `""` instead. So this assertion also records *which* implementation the + * unit tests run against. + */ + @Test + fun `optString on an explicit null gives the four letter text null`() { + val doc = JSONObject("""{"nulled":null}""") + + assertThat(doc.optString("nulled")).isEqualTo("null") + assertThat(doc.optString("nulled")).hasLength(4) + } + + /** + * The dead branch in `NSClientV3Service.onDataDelete`, isolated. + * + * After the move to kotlinx this elvis would start firing, which **changes behaviour**: today a + * delete event with no `colName` carries on with an empty collection name and quietly matches + * none of the `if (collection == ...)` branches. Fix it on purpose or preserve it on purpose, + * but do not let a rewrite decide it by accident. + */ + @Test + fun `elvis after optString is dead code - current behaviour, pinned`() { + val doc = JSONObject("""{"identifier":"abc"}""") + + var elvisFired = false + val collection = doc.optString("colName") ?: run { elvisFired = true; "bailed" } + + assertThat(elvisFired).isFalse() + assertThat(collection).isEqualTo("") + } + + // ---------------------------------------------------------------- optJSONObject + + /** Unlike `optString`, this one really does return null, so the `?:` guards on it are live. */ + @Test + fun `optJSONObject returns null for a missing key`() { + val doc = JSONObject("""{"envelope":{"a":1}}""") + + assertThat(doc.optJSONObject("envelope")).isNotNull() + assertThat(doc.optJSONObject("missing")).isNull() + } + + /** A key holding a non-object gives null rather than throwing. */ + @Test + fun `optJSONObject returns null when the value is not an object`() { + val doc = JSONObject("""{"notAnObject":"plain string"}""") + + assertThat(doc.optJSONObject("notAnObject")).isNull() + } + + // ---------------------------------------------------------------- optLong / optBoolean + + /** `LoadSettingsWorker` and `onDataCreateUpdate` both pass an explicit 0 default. */ + @Test + fun `optLong falls back to the supplied default`() { + val doc = JSONObject("""{"srvModified":1785992181588}""") + + assertThat(doc.optLong("srvModified", 0L)).isEqualTo(1785992181588L) + assertThat(doc.optLong("missing", 0L)).isEqualTo(0L) + } + + /** A string that looks like a number is coerced, it does not fall back to the default. */ + @Test + fun `optLong coerces a numeric string`() { + val doc = JSONObject("""{"srvModified":"1785992181588"}""") + + assertThat(doc.optLong("srvModified", 0L)).isEqualTo(1785992181588L) + } + + /** Used for the socket.io subscribe/auth replies in `NSClientV3Service`. */ + @Test + fun `optBoolean returns false for a missing key`() { + val doc = JSONObject("""{"success":true}""") + + assertThat(doc.optBoolean("success")).isTrue() + assertThat(doc.optBoolean("missing")).isFalse() + } + + // ---------------------------------------------------------------- classpath guard + + /** + * Proves the real `org.json` is on the test classpath and not the `android.jar` stub. + * + * With the stub plus `isReturnDefaultValues = true` every call here would return null / 0 / + * false and the assertions above would be vacuous. + */ + @Test + fun `the real org json implementation is on the test classpath`() { + val doc = JSONObject("""{"a":1,"b":"two"}""") + + assertThat(doc.length()).isEqualTo(2) + assertThat(doc.getInt("a")).isEqualTo(1) + assertThat(doc.getString("b")).isEqualTo("two") + assertThat(doc.has("a")).isTrue() + assertThat(doc.toString()).contains("\"a\"") + } +} diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt index 05b2c9a662c0..ab841e4dd28b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt @@ -49,7 +49,11 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.put import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -226,13 +230,13 @@ internal class ClientControlReceiverTest { SignedEnvelope(clientId = clientId, counter = counter, timestamp = timestamp, type = type, payload = payload, signature = "", validUntil = validUntil, wantsAck = wantsAck) ) - private fun wrap(envelope: SignedEnvelope): JSONObject = - JSONObject().apply { + private fun wrap(envelope: SignedEnvelope): JsonObject = + buildJsonObject { put("date", envelope.timestamp) put("utcOffset", 0) put("app", "AAPS") put("schemaVersion", 1) - put("envelope", JSONObject(Json.encodeToString(SignedEnvelope.serializer(), envelope))) + put("envelope", Json.encodeToJsonElement(SignedEnvelope.serializer(), envelope)) } private val deleteOk = CreateUpdateResponse(response = 200, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null, errorResponse = null) @@ -395,7 +399,7 @@ internal class ClientControlReceiverTest { fun missingEnvelopeFieldIsIgnoredNotDeleted() = runTest { pair() val identifier = ClientControlPublisher.IDENTIFIER_HELLO_PREFIX + "anything" - val noEnvelope = JSONObject().apply { + val noEnvelope = buildJsonObject { put("date", now) put("app", "AAPS") } @@ -410,7 +414,7 @@ internal class ClientControlReceiverTest { fun malformedEnvelopeJsonIsIgnoredNotDeleted() = runTest { pair() val identifier = ClientControlPublisher.IDENTIFIER_HELLO_PREFIX + "anything" - val malformed = JSONObject().apply { put("envelope", JSONObject().apply { put("garbage", true) }) } + val malformed = buildJsonObject { put("envelope", buildJsonObject { put("garbage", true) }) } sut.onSettingsDocChanged(identifier, malformed) @@ -824,11 +828,12 @@ internal class ClientControlReceiverTest { val (clientId, secret) = pair() authorizedRepository.markActive(clientId, counterReceived = 1L, now = now - 5_000L) val cmdIdentifier = "${ClientControlPublisher.IDENTIFIER_CMD_PREFIX}scene_stop_$clientId" - val cmdDoc = wrap(envelope(clientId, secret, message = ClientControlMessage.SceneStop(false), counter = 5L)).also { - it.put("identifier", cmdIdentifier) - } + // kotlinx JsonObject is immutable, so the identifier goes on via a copy. + val cmdDoc = JsonObject( + wrap(envelope(clientId, secret, message = ClientControlMessage.SceneStop(false), counter = 5L)) + ("identifier" to JsonPrimitive(cmdIdentifier)) + ) // Mix in a non-clientcontrol doc that polling should ignore. - val unrelatedDoc = JSONObject().apply { + val unrelatedDoc = buildJsonObject { put("identifier", "aaps") put("date", now) } @@ -851,10 +856,10 @@ internal class ClientControlReceiverTest { // Master writes its own pairing-offer docs under IDENTIFIER_OFFER_PREFIX. Without an // explicit skip, verifyAndAck would treat them as envelopes, find no `envelope` field, // and delete a still-live offer mid-pairing — leaving the client unable to fetch it. - val offerDoc = JSONObject().apply { + val offerDoc = buildJsonObject { put("identifier", "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}any-client-id") put("date", now) - put("offer", JSONObject().apply { put("clientId", "any-client-id") }) + put("offer", buildJsonObject { put("clientId", "any-client-id") }) } whenever(nsAndroidClient.searchSettings(limit = 100)).thenReturn( NSAndroidClient.ReadResponse(code = 200, lastServerModified = null, values = listOf(offerDoc)) @@ -868,7 +873,7 @@ internal class ClientControlReceiverTest { @Test fun wsPushSkipsOfferDocs() = runTest { val identifier = "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}any-client-id" - val doc = JSONObject().apply { put("identifier", identifier); put("date", now) } + val doc = buildJsonObject { put("identifier", identifier); put("date", now) } sut.onSettingsDocChanged(identifier, doc) verify(nsAndroidClient, never()).deleteSettings(any()) } @@ -881,9 +886,9 @@ internal class ClientControlReceiverTest { // never execute it. This locks in the fix against re-delivery via the poll fallback. pair() val identifier = "${ClientControlPublisher.IDENTIFIER_CMD_PREFIX}scene_stop_stranger-uuid" - val doc = wrap(envelope("stranger-uuid", ByteArray(32) { 0x11 }, message = ClientControlMessage.SceneStop(false), counter = 5L)).also { - it.put("identifier", identifier) - } + val doc = JsonObject( + wrap(envelope("stranger-uuid", ByteArray(32) { 0x11 }, message = ClientControlMessage.SceneStop(false), counter = 5L)) + ("identifier" to JsonPrimitive(identifier)) + ) whenever(nsAndroidClient.searchSettings(limit = 100)).thenReturn( NSAndroidClient.ReadResponse(code = 200, lastServerModified = null, values = listOf(doc)) ) @@ -901,8 +906,8 @@ internal class ClientControlReceiverTest { private suspend fun captureAcks(clientId: String): MutableList { val acks = mutableListOf() whenever(nsAndroidClient.updateSettings(eq(ClientControlPublisher.IDENTIFIER_ACK_PREFIX + clientId), any())).thenAnswer { - val doc = it.getArgument(1) - acks += Json.decodeFromString(doc.getJSONObject("ack").toString()) + val doc = it.getArgument(1) + acks += Json.decodeFromString(doc["ack"]!!.jsonObject.toString()) deleteOk } return acks diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt index 2004b586cf3a..c24d4e172517 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt @@ -37,7 +37,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -178,12 +179,12 @@ internal class ClientControlRoundTripTest { private fun progressDoc( phase: ProgressPhase, percent: Int = 0, status: String = "", insulin: Double = 2.0, delivered: Double = 0.0, ts: Long = now, signSecret: ByteArray = secret - ): JSONObject { + ): JsonObject { val env = ClientControlCrypto.signProgress( signSecret, ProgressEnvelope(clientId, phase, insulin, percent, status, delivered, stopDeliveryEnabled = true, timestamp = ts, signature = "") ) - return JSONObject().apply { put("progress", JSONObject(json.encodeToString(ProgressEnvelope.serializer(), env))) } + return buildJsonObject { put("progress", json.encodeToJsonElement(ProgressEnvelope.serializer(), env)) } } private suspend fun stubPublish(result: ClientControlSendResult, ctr: Long? = counter) { @@ -191,9 +192,9 @@ internal class ClientControlRoundTripTest { } /** A signed ACK doc as the WS layer would hand it to onAckDoc. [signSecret] lets a test forge one. */ - private fun ackDoc(phase: AckPhase, status: AckStatus, reason: String? = null, payload: String? = null, ctr: Long = counter, signSecret: ByteArray = secret): JSONObject { + private fun ackDoc(phase: AckPhase, status: AckStatus, reason: String? = null, payload: String? = null, ctr: Long = counter, signSecret: ByteArray = secret): JsonObject { val ack = ClientControlCrypto.signAck(signSecret, AckEnvelope(clientId, ctr, phase, status, reason, payload, timestamp = now, signature = "")) - return JSONObject().apply { put("ack", JSONObject(json.encodeToString(AckEnvelope.serializer(), ack))) } + return buildJsonObject { put("ack", json.encodeToJsonElement(AckEnvelope.serializer(), ack)) } } @Test diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt index ee58c8825d2f..39b5bab00125 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt @@ -41,7 +41,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -134,7 +134,7 @@ class ClientControlUplinkIntegrationTest { // -- NS bridge (the settings collection) -- private var bridgeIdentifier: String? = null - private var bridgeDoc: JSONObject? = null + private var bridgeDoc: JsonObject? = null @BeforeEach fun setUp() = runBlocking { @@ -175,7 +175,7 @@ class ClientControlUplinkIntegrationTest { // bridged back here, so the round-trip just times out to Unconfirmed — the command doc is still // captured + delivered to the master, which is what this uplink test asserts. whenever(nsClientV3Plugin.masterReachable).thenReturn(MutableStateFlow(true)) - whenever(nsAndroidClient.updateSettings(any(), any())).thenAnswer { + whenever(nsAndroidClient.updateSettings(any(), any())).thenAnswer { val id = it.getArgument(0) // The master now writes two-step ACK docs through this same NS bridge; ignore them here so // the capture reflects only the client's command publish (the uplink under test). diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcherTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcherTest.kt index b30177cce49c..0066863c9242 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcherTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcherTest.kt @@ -12,7 +12,9 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -71,7 +73,7 @@ internal class PairingOfferFetcherTest { payload: PairingPayload = payload(), identifier: String = "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}${payload.clientId}", expiresAt: Long = now + 120_000L - ): JSONObject { + ): JsonObject { val salt = ClientControlPairingCrypto.newSalt() val iv = ClientControlPairingCrypto.newIv() val plaintext = Json.encodeToString(PairingPayload.serializer(), payload).toByteArray() @@ -83,13 +85,13 @@ internal class PairingOfferFetcherTest { ivB64 = b64(iv), wrappedB64 = b64(wrapped) ) - return JSONObject().apply { + return buildJsonObject { put("identifier", identifier) - put("offer", JSONObject(Json.encodeToString(PairingOffer.serializer(), offer))) + put("offer", Json.encodeToJsonElement(PairingOffer.serializer(), offer)) } } - private suspend fun respond(vararg docs: JSONObject) { + private suspend fun respond(vararg docs: JsonObject) { whenever(nsAndroidClient.searchSettings(limit = 500)).thenReturn( NSAndroidClient.ReadResponse(code = 200, lastServerModified = null, values = docs.toList()) ) @@ -146,7 +148,7 @@ internal class PairingOfferFetcherTest { @Test fun foreignDocsDoNotBlockAValidOffer() = runTest { - val unrelated = JSONObject().apply { put("identifier", "aaps"); put("date", now) } + val unrelated = buildJsonObject { put("identifier", "aaps"); put("date", now) } val valid = payload(clientId = "client-A") respond(unrelated, offerDoc(pin = "12345678", payload = valid)) val result = sut.findOfferForPin("12345678") @@ -156,9 +158,9 @@ internal class PairingOfferFetcherTest { @Test fun skipsMalformedOffer() = runTest { - val malformed = JSONObject().apply { + val malformed = buildJsonObject { put("identifier", "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}broken") - put("offer", JSONObject().apply { put("clientId", "broken") }) // missing required wrapped/iv/salt + put("offer", buildJsonObject { put("clientId", "broken") }) // missing required wrapped/iv/salt } respond(malformed) assertThat(sut.findOfferForPin("12345678")).isEqualTo(PairingOfferFetcher.Result.NoMatch) @@ -166,7 +168,7 @@ internal class PairingOfferFetcherTest { @Test fun skipsDocWithoutOfferObject() = runTest { - val noOffer = JSONObject().apply { put("identifier", "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}x") } + val noOffer = buildJsonObject { put("identifier", "${ClientControlPublisher.IDENTIFIER_OFFER_PREFIX}x") } respond(noOffer) assertThat(sut.findOfferForPin("12345678")).isEqualTo(PairingOfferFetcher.Result.NoMatch) } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt new file mode 100644 index 000000000000..63b681756e7f --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt @@ -0,0 +1,204 @@ +package app.aaps.plugins.sync.nsclientV3.json + +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optBooleanCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonArrayCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optLongCompat +import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import org.json.JSONObject +import org.junit.jupiter.api.Test + +/** + * Golden master test for [OrgJsonCompat]. + * + * Every case below is parsed **twice**, once with `org.json` and once with kotlinx, and the two + * results are required to match. `org.json` is the reference: whatever it does today is what the + * replacement has to keep doing, quirks included. + * + * The point is that the accessor differences are invisible to the compiler. If `optString` starts + * returning null for a missing key nothing fails to build - a downstream `isEmpty()` just quietly + * takes the other branch. This test is what makes that visible. + */ +class OrgJsonCompatTest { + + /** One row of the matrix: a document, and the key to read out of it. */ + private data class Case(val name: String, val json: String, val key: String) + + private val cases = listOf( + Case("missing key", """{"other":1}""", "k"), + Case("explicit null", """{"k":null}""", "k"), + Case("plain string", """{"k":"hello"}""", "k"), + Case("empty string", """{"k":""}""", "k"), + Case("string with spaces", """{"k":"a b c"}""", "k"), + Case("string that looks numeric", """{"k":"1785992181588"}""", "k"), + Case("string true", """{"k":"true"}""", "k"), + Case("string TRUE upper", """{"k":"TRUE"}""", "k"), + Case("string false", """{"k":"false"}""", "k"), + Case("int", """{"k":42}""", "k"), + Case("zero", """{"k":0}""", "k"), + Case("negative int", """{"k":-6}""", "k"), + Case("big long", """{"k":1785992181588}""", "k"), + Case("double", """{"k":0.692}""", "k"), + Case("negative double", """{"k":-0.411}""", "k"), + Case("boolean true", """{"k":true}""", "k"), + Case("boolean false", """{"k":false}""", "k"), + Case("nested object", """{"k":{"a":1}}""", "k"), + Case("empty object", """{"k":{}}""", "k"), + Case("array", """{"k":[1,2,3]}""", "k"), + Case("empty array", """{"k":[]}""", "k") + ) + + private fun orgJson(case: Case) = JSONObject(case.json) + private fun kotlinxJson(case: Case): JsonObject = Json.parseToJsonElement(case.json).jsonObject + + // ---------------------------------------------------------------- the matrix + + @Test + fun `optString matches org json for every case`() { + for (case in cases) + assertWithMessage("optString - %s", case.name) + .that(kotlinxJson(case).optStringCompat(case.key)) + .isEqualTo(orgJson(case).optString(case.key)) + } + + @Test + fun `optLong matches org json for every case`() { + for (case in cases) + assertWithMessage("optLong 0 - %s", case.name) + .that(kotlinxJson(case).optLongCompat(case.key, 0L)) + .isEqualTo(orgJson(case).optLong(case.key, 0L)) + } + + @Test + fun `optLong honours a non zero fallback for every case`() { + for (case in cases) + assertWithMessage("optLong -1 - %s", case.name) + .that(kotlinxJson(case).optLongCompat(case.key, -1L)) + .isEqualTo(orgJson(case).optLong(case.key, -1L)) + } + + @Test + fun `optBoolean matches org json for every case`() { + for (case in cases) + assertWithMessage("optBoolean - %s", case.name) + .that(kotlinxJson(case).optBooleanCompat(case.key)) + .isEqualTo(orgJson(case).optBoolean(case.key)) + } + + @Test + fun `optJSONObject presence matches org json for every case`() { + for (case in cases) { + val expected = orgJson(case).optJSONObject(case.key) + val actual = kotlinxJson(case).optJsonObjectCompat(case.key) + + assertWithMessage("optJSONObject present - %s", case.name) + .that(actual != null).isEqualTo(expected != null) + if (expected != null && actual != null) + assertWithMessage("optJSONObject keys - %s", case.name) + .that(actual.keys).isEqualTo(expected.keys().asSequence().toSet()) + } + } + + @Test + fun `optJSONArray presence matches org json for every case`() { + for (case in cases) { + val expected = orgJson(case).optJSONArray(case.key) + val actual = kotlinxJson(case).optJsonArrayCompat(case.key) + + assertWithMessage("optJSONArray present - %s", case.name) + .that(actual != null).isEqualTo(expected != null) + if (expected != null && actual != null) + assertWithMessage("optJSONArray size - %s", case.name) + .that(actual.size).isEqualTo(expected.length()) + } + } + + // ---------------------------------------------------------------- real call site shapes + + /** + * The shapes the NSClientV3 code actually reads, taken from the live call sites, checked against + * `org.json` end to end rather than one accessor at a time. + */ + @Test + fun `real NSClient document shapes match org json`() { + val documents = listOf( + """{"colName":"treatments","identifier":"abc123"}""", + """{"colName":"entries"}""", + """{"identifier":"abc123"}""", + """{}""", + """{"success":true,"collections":"treatments entries"}""", + """{"success":false,"message":"not authorised"}""", + """{"message":null,"level":"urgent","title":"Alarm"}""", + """{"srvModified":1785992181588,"identifier":"cold"}""", + """{"envelope":{"cmd":"bolus","amount":1.5},"identifier":"ctrl_1"}""", + """{"ack":{"ok":true},"progress":{"percent":40}}""" + ) + val keys = listOf( + "colName", "identifier", "success", "collections", "message", + "level", "title", "srvModified", "envelope", "ack", "progress" + ) + + for (document in documents) { + val reference = JSONObject(document) + val candidate = Json.parseToJsonElement(document).jsonObject + for (key in keys) { + // Skip optString on object valued keys - see `optString on an object differs only + // in key order` below. No call site reads an object through optString. + if (reference.optJSONObject(key) == null) + assertWithMessage("optString(%s) of %s", key, document) + .that(candidate.optStringCompat(key)).isEqualTo(reference.optString(key)) + assertWithMessage("optLong(%s) of %s", key, document) + .that(candidate.optLongCompat(key, 0L)).isEqualTo(reference.optLong(key, 0L)) + assertWithMessage("optBoolean(%s) of %s", key, document) + .that(candidate.optBooleanCompat(key)).isEqualTo(reference.optBoolean(key)) + assertWithMessage("optJSONObject(%s) of %s", key, document) + .that(candidate.optJsonObjectCompat(key) != null) + .isEqualTo(reference.optJSONObject(key) != null) + } + } + } + + /** + * The one place the two libraries genuinely disagree, recorded on purpose. + * + * Reading an **object** through `optString` serialises the subtree, and `org.json` iterates a + * hash map, so its key order is unspecified and does not follow the source text. kotlinx keeps + * source order. Asserting equality of those two strings would be pinning nondeterminism, so the + * matrix skips this combination. + * + * It is safe to skip because no call site does it: all six keys read through `optString` + * (`identifier`, `colName`, `collections`, `message`, `level`, `title`) hold strings. Object + * valued keys are read through `optJSONObject`, which agrees exactly. + * + * What must still hold is that neither side loses anything - both produce text that parses back + * to the same object. + */ + @Test + fun `optString on an object differs only in key order`() { + val document = """{"envelope":{"cmd":"bolus","amount":1.5,"id":"x"}}""" + val reference = JSONObject(document).optString("envelope") + val candidate = Json.parseToJsonElement(document).jsonObject.optStringCompat("envelope") + + // Same content, reparsed - order is not part of the contract. + assertThat(Json.parseToJsonElement(candidate).jsonObject) + .isEqualTo(Json.parseToJsonElement(reference).jsonObject) + } + + /** + * `startsWith` on the result is how `NSClientV3Service` routes client control documents. With + * `org.json` the receiver is never null, so this cannot throw - the replacement has to keep that. + */ + @Test + fun `startsWith on a missing identifier does not throw`() { + val candidate = Json.parseToJsonElement("""{"other":1}""").jsonObject + + val identifier = candidate.optStringCompat("identifier") + assertThat(identifier).isEqualTo("") + assertThat(identifier.startsWith("ctrl_")).isFalse() + } +} diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt index 3091846d26f2..fc250651ee66 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt @@ -27,7 +27,8 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest -import org.json.JSONObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.anyBoolean @@ -105,9 +106,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.newestDataOnServer?.collections?.profile = Long.MAX_VALUE sut = buildSut() - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) put("srvModified", now - 1000) } whenever(nsAndroidClient.getLastProfileStore()) @@ -129,9 +130,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.newestDataOnServer?.collections?.profile = now sut = buildSut() - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) .thenReturn(NSAndroidClient.ReadResponse(200, now - 1000, listOf(profile))) @@ -152,9 +153,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.newestDataOnServer?.collections?.profile = now sut = buildSut() - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) .thenReturn(NSAndroidClient.ReadResponse(200, now - 1000, listOf(profile))) @@ -174,9 +175,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.newestDataOnServer?.collections?.profile = now sut = buildSut() - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) put("srvModified", now - 500) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) @@ -198,9 +199,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { sut = buildSut() val createdAt = dateUtil.toISOString(now - 300) - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) put("created_at", createdAt) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) @@ -255,14 +256,14 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.newestDataOnServer?.collections?.profile = now sut = buildSut() - val profile1 = JSONObject().apply { + val profile1 = buildJsonObject { put("defaultProfile", "Profile1") - put("store", JSONObject()) + put("store", buildJsonObject { }) put("srvModified", now - 1000) } - val profile2 = JSONObject().apply { + val profile2 = buildJsonObject { put("defaultProfile", "Profile2") - put("store", JSONObject()) + put("store", buildJsonObject { }) put("srvModified", now - 500) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) @@ -307,9 +308,9 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { nsClientV3Plugin.doingFullSync = true sut = buildSut() - val profile = JSONObject().apply { + val profile = buildJsonObject { put("defaultProfile", "Default") - put("store", JSONObject()) + put("store", buildJsonObject { }) } whenever(nsAndroidClient.getProfileModifiedSince(anyLong())) .thenReturn(NSAndroidClient.ReadResponse(200, now - 1000, listOf(profile))) From 350f486be4219f3bfb6106cfafa5f5027c6033db Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 13:58:28 +0200 Subject: [PATCH 006/146] :core:nssdk Gson -> kotlinx.serialization --- _docs/KMP_IOS_FEASIBILITY.md | 170 +++++++++++++++++- core/nssdk/build.gradle.kts | 4 +- .../aaps/core/nssdk/NSAndroidClientImpl.kt | 9 +- .../kotlin/app/aaps/core/nssdk/NsSdkJson.kt | 63 +++++++ .../nssdk/localmodel/treatment/EventType.kt | 62 +++---- .../localmodel/treatment/NSTherapyEvent.kt | 7 +- .../core/nssdk/mapper/DeviceStatusMapper.kt | 27 +-- .../app/aaps/core/nssdk/mapper/FoodMapper.kt | 4 +- .../app/aaps/core/nssdk/mapper/MbgMapper.kt | 4 +- .../app/aaps/core/nssdk/mapper/SvgMapper.kt | 4 +- .../aaps/core/nssdk/mapper/TreatmentMapper.kt | 4 +- .../nssdk/networking/NetworkStackBuilder.kt | 34 +--- .../networking/NightscoutRemoteService.kt | 12 +- .../core/nssdk/remotemodel/LastModified.kt | 16 +- .../nssdk/remotemodel/RemoteAuthResponse.kt | 8 + .../nssdk/remotemodel/RemoteDeviceStatus.kt | 65 ++++--- .../core/nssdk/remotemodel/RemoteEntry.kt | 53 +++--- .../aaps/core/nssdk/remotemodel/RemoteFood.kt | 54 +++--- .../aaps/core/nssdk/remotemodel/RemoteICfg.kt | 21 ++- .../nssdk/remotemodel/RemoteStatusResponse.kt | 44 +++-- .../core/nssdk/remotemodel/RemoteTreatment.kt | 132 +++++++------- .../aaps/core/nssdk/NsSdkWireFormatTest.kt | 151 ++++++++++++++++ .../nssdk/mapper/DeviceStatusMapperTest.kt | 42 ++--- .../nssdk/mapper/FoodAndEntryToleranceTest.kt | 137 ++++++++++++++ .../nssdk/mapper/KotlinxCoercionSpikeTest.kt | 129 +++++++++++++ .../mapper/RealNightscoutTreatmentTest.kt | 39 ++-- .../core/nssdk/mapper/WireTypeCoercionTest.kt | 144 +++++++++++++++ gradle/libs.versions.toml | 1 + 28 files changed, 1128 insertions(+), 312 deletions(-) create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 883c280d05f9..f521b7f58c76 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -60,6 +60,7 @@ So most of the Compose work of the last year can be reused. | Retrofit + OkHttp | 6 | Ktor client | | socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | | Gson | 34 | kotlinx.serialization | +| `org.json` (`JSONObject`) | 243 | kotlinx `JsonObject` - see section 8 | | WorkManager | 44 | See warning below | | joda-time | 5 | kotlinx-datetime | | `java.text.DecimalFormat` | 74 | Done, see section 8 | @@ -372,6 +373,13 @@ Step 0 proves the toolchain works, gives an honest answer about how Compose Mult iOS, and makes every later step demand driven: a blocker is removed because it stands between you and the next screen, not because it is on a list. +**Where this stands.** Step 1 is done. Half of step 0 is done too, and out of order: `:core:data` +already builds for Kotlin/Native (wave 5), and part of step 5 has been pulled forward because +`:core:nssdk` turned out to be sliceable after all (wave 6). What is still missing from step 0 is the +half that needs a Mac - a real device, and an honest look at Compose Multiplatform on iOS. No amount +of further blocker removal answers that question, which is the argument for doing it soon rather than +continuing down the list. + --- ## 8. Work done so far @@ -383,6 +391,17 @@ Committed on `dev`: | `e5f4e27626` | Migrate DecimalFormat | | `a42d823c93` | Eliminate TimeUnit | | `e1068e77db` | `:core:keys` remove JVM dependency | +| `35b5399798` | Extract dependencies | +| `1aed547f7a` | cleanup (`TB.isInProgress` moved out of `:core:data`) | + +Committed on `kmp/core-data-experiment`, a throwaway branch kept because the work turned out to be +worth keeping (see waves 5 and 6): + +| Commit | What | +| --- | --- | +| `67ecb1e696` | Going KMP - `:core:data` is a real multiplatform module | +| `e24476237b` | Prepare `:core:nssdk` - dead code out, defaults in, characterization tests | +| `f6a4e85e8a` | `:core:nssdk` `org.json` -> kotlinx | ### Wave 1 - `DecimalFormat` removed @@ -605,6 +624,123 @@ creation is the weaker case, and the 154 call sites that care already pass `utcO Two `expect` / `actual` declarations away from compiling as `commonMain`. +### Wave 5 - `:core:data` really is multiplatform (done) + +`:core:data` now uses `kotlin("multiplatform")` with a `jvm()` target and `mingwX64()` as a stand-in +for iOS (real iOS targets need macOS and Xcode, so Windows cannot build them - `mingwX64` proves the +code compiles for Kotlin/Native, which is the part that was in doubt). + +**Zero consumer changes.** All 13 modules that depend on `:core:data` build unmodified: an Android +consumer still sees a normal library, because a multiplatform module publishes a normal variant. +That was the single most valuable thing to learn, and the reason it was worth doing on a branch. + +Two `expect` / `actual` seams, both deliberate: + +| Seam | Why | +| --- | --- | +| `NumberFormatPlatform` | number formatting is genuinely platform work | +| `systemUtcOffsetAt(timestamp)` | replaces `TimeZone.getDefault()` in 17 model files, no call site changed | + +The `mingwX64` target needed one opt-in, `kotlin.experimental.ExperimentalNativeApi`, because +`ICfg.iobCalcForTreatment` uses `assert()`. + +Verified on WSA: all four flavours install, run, and sync against a live Nightscout. + +### Wave 6 - `:core:nssdk` off `org.json` (done) + +**Worth recording, because the first assessment was wrong.** The module was written off as something +that could not be salami-sliced: the Gson annotations, the joda date parsing, the `IOException` +hierarchy and the converter all form one wire-format contract, so the argument went that it had to +move in a single step or not at all. That is true *of the converter*, but the converter is not the +only seam. The carrier type is a separate one, and it came out first, alone, with the converter +untouched. The lesson generalises: "these things are coupled" is a claim about one axis, and it is +worth checking whether some other axis cuts cleanly before accepting a big-bang migration. + +`org.json` is a JVM and Android API. It was in the **public API** of `NSAndroidClient` (10 methods) +and `RunningConfiguration` (2), carrying profiles and settings - the two document kinds AAPS does not +model, because it does not own their shape. It is now gone from the module. + +Why it could be done alone: both directions already round-tripped through text, so `org.json` was +only ever a carrier. + +```kotlin +JSONObject(json.asJsonObject.toString()) // read - Gson tree -> text -> org.json +api.createSetting(JsonParser.parseString(doc.toString())) // write - org.json -> text -> Gson tree +``` + +Swapping the carrier for kotlinx `JsonObject` left the write line **character for character +identical** and changed one parse call on the read side. Gson, Retrofit and the 210 `@SerializedName` +annotations were not touched. + +**The trap, and why tests came first.** The two libraries disagree about a missing key, and nothing +about the difference shows up at compile time: + +| accessor | `org.json`, missing key | `org.json`, explicit `null` | kotlinx, missing key | +| --- | --- | --- | --- | +| `optString` | `""` | `"null"` - the four letter text | `null` | +| `optJSONObject` | `null` | `null` | `null` | +| `optLong(k, 0)` | `0` | `0` | `null` | +| `optBoolean` | `false` | `false` | `null` | + +A straight translation would silently flip every downstream `isEmpty()` and `?:`. So the swap was +done through `OrgJsonCompat`, kotlinx accessors that reproduce `org.json` exactly, golden-mastered +against the real thing over 21 inputs x 5 accessors. The type change is then equivalent by +construction rather than by inspection. + +The one genuine divergence: reading an **object** through `optString` differs in key order, because +`org.json` iterates a hash map and its order is unspecified. No call site does it - all six keys read +through `optString` hold strings - so it is documented and skipped rather than pinned. + +**What stays on `org.json`, on purpose.** `JsonBridge` marks the two boundaries: + +- **socket.io** hands every payload over as `org.json.JSONObject`. That is the library's API, so the + conversion happens as the event arrives and everything downstream is kotlinx. +- **The profile subsystem** - `ProfileStore`, `PureProfile`, `DataSyncSelector.PairProfileStore` - is + `org.json` throughout `:core:interfaces`. Converting it is its own project. Profiles are converted + where they cross into the client instead. + +Both live in `:plugins:sync`, which is Android only by nature (WorkManager, a socket.io `Service`), +so the `org.json` quirks stay out of the shared modules. + +**Dead code removed on the way:** + +- `RemoteProfileStore` - an abandoned attempt to model the profile store as a typed class, with a + commented-out `Store` / `SimpleProfile` / `ProfileEntry` model still in the file. Zero consumers. + This was the "nested JSON I could not get working with kotlinx" that came up in discussion - the + archaeology was still in the file. +- `NSAndroidRxClient` - dead, and `kotlinx.coroutines.rx3` went with it. +- Both `?: return@Listener` guards in `NSClientV3Service.onDataDelete`. `optString` never returns + null, so neither could ever fire; Kotlin allowed them only because `org.json` is Java and the type + is the platform type `String!`. + +**Immutability was the only real code change.** kotlinx `JsonObject` cannot be mutated, so six +in-place `put` calls became rebuilds. Five were mechanical; the sixth was not - +`RunningConfigurationPublisher` mutated a *nested* object to mask `NsClientAllowClientControl` inside +`syncedPrefs`, and that is now an explicit rebuild preserving key order and the masking semantics. + +The wire format's mixed typing was preserved deliberately: `isFakingTempsByExtendedBoluses` is a real +JSON boolean while every `syncedPrefs` value is a string, exactly as `org.json` wrote them. + +**Left unfixed, on purpose.** `optStringCompat` faithfully reproduces the `"null"` quirk, so a server +sending `{"message":null}` still writes the word "null" into the user visible NSClient log. Changing +behaviour during a type migration is how regressions get smuggled in; that is a separate change. + +**What is left in `:core:nssdk`:** + +| Dependency | Files | Slice | +| --- | --- | --- | +| Gson | 17 | the converter switch | +| joda-time | 1 | with the converter - `RemoteTreatment` lenient dates | +| Retrofit | 4 | Ktor | +| OkHttp | 3 | Ktor | +| `java.io.IOException` | 1 | with Ktor - the exception hierarchy | +| `android.*` | 2 | with Ktor - mainly `Context` for the OkHttp disk cache | + +Verified on WSA with a full sync. That specifically exercises the riskiest part: settings and profile +reads both go through the changed Gson type adapter, and kotlinx `JsonObject` also implements +`Map`, so Gson picking its built-in map adapter instead of the registered one +was a real possibility that would have failed quietly rather than thrown. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old @@ -654,17 +790,35 @@ keeps the old contract on a public interface method. in 57 files, and most of it sits in pump drivers that will never be KMP. Do it in the modules on the KMP path, leave `:pump:*` and `:wear` alone, and make it a rule for new code so the number stops growing. -5. **Still open: which branch for the first KMP module.** Converting `:core:data` to - `kotlin("multiplatform")` changes how Gradle resolves it for the 13 modules that depend on it. - That is the most valuable thing to find out and the most likely thing to break, so a throwaway - branch is safer than `dev`. +5. ~~Which branch for the first KMP module?~~ **Decided: `kmp/core-data-experiment`, and it worked.** + The fear was that converting `:core:data` to `kotlin("multiplatform")` would change how Gradle + resolves it for the 13 dependent modules. It does not - a multiplatform module still publishes a + normal Android variant, and all 13 built unmodified. The branch was meant to be thrown away and is + now worth merging. 6. **Still open: `wear/SmallestDoubleString.kt`** - the last `DecimalFormat` in a non-test file that is not the deliberate platform seam. It builds patterns from a runtime string, so it needs real logic rather than a mapping. - -Waves 1 to 3 are committed. Still in the working tree: the `TB.isInProgress` extension and the -`DoseStepSize` change from Wave 4. The `FoodManagement` comma defect in section 10 is found but not -fixed. +7. **Still open: the `:core:nssdk` converter switch.** Gson to kotlinx is atomic - 210 + `@SerializedName`, the joda date parsing and the `IOException` hierarchy move together. Two things + need deciding first: the `Json { }` configuration (`ignoreUnknownKeys = true`, `explicitNulls = + false`), and what to do about the ~8 **non-null** fields that were deliberately left without + defaults. Gson currently leaves those null through `Unsafe.allocateInstance`; kotlinx would throw. + For `RemoteStatusResponse` throwing is arguably correct, since `v3/status` always sends them. + `RemoteFood` from a foreign uploader is the real question. +8. **Still open: Retrofit converter, or straight to Ktor?** Adding + `retrofit2-kotlinx-serialization-converter` is the smaller step but adds a dependency that gets + thrown away when Ktor lands. Going straight to Ktor avoids that and is the actual destination, and + its failure mode ("no network") is louder than a serialization-only swap ("quietly wrong data"). + Leaning Ktor. +9. **Still open: the OkHttp disk cache needs a `Context`.** One of the two remaining `android.*` + imports in `:core:nssdk`. Ktor on iOS needs either a different cache story or none. +10. **Still open: R8 has never run against the KMP module.** Both device checks were debug builds, so + minification against multiplatform metadata and the `expect` / `actual` pairs is untested. This is + the only real remaining risk on the branch. + +Waves 1 to 4 are committed on `dev`. Waves 5 and 6 are committed on `kmp/core-data-experiment`, both +verified on WSA against a live Nightscout. The `FoodManagement` comma defect in section 10 is found +but not fixed. --- diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 84d96e175e3e..4dcb51a41929 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -12,7 +12,9 @@ android { dependencies { implementation(libs.com.squareup.retrofit2.retrofit) - implementation(libs.com.squareup.retrofit2.converter.gson) + // Official Retrofit 3 artifact, same group and version as the Gson converter it replaces. + // Goes away with Retrofit itself when the client moves to Ktor. + implementation(libs.com.squareup.retrofit2.converter.kotlinx.serialization) api(libs.com.squareup.okhttp3.okhttp) api(libs.com.squareup.okhttp3.logging.interceptor) api(libs.net.danlew.android.joda) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt index 5130a7b1c9ff..741caa436e17 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt @@ -33,7 +33,6 @@ import app.aaps.core.nssdk.remotemodel.RemoteFood import app.aaps.core.nssdk.remotemodel.RemoteTreatment import app.aaps.core.nssdk.utils.retry import app.aaps.core.nssdk.utils.toNotNull -import com.google.gson.JsonParser import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -505,7 +504,7 @@ class NSAndroidClientImpl( // kotlinx JsonObject is immutable, so the app name goes into a copy instead of being put in // place. Same result on the wire: an existing "app" key is replaced, a new one is added. val stamped = JsonObject(remoteProfileStore + ("app" to JsonPrimitive("AAPS"))) - val response = api.createProfile(JsonParser.parseString(stamped.toString()).asJsonObject) + val response = api.createProfile(stamped) if (response.isSuccessful) { if (response.code() == 200 || response.code() == 201) { return@callWrapper CreateUpdateResponse( @@ -609,7 +608,7 @@ class NSAndroidClientImpl( // See createProfileStore: kotlinx JsonObject is immutable, so stamp a copy. val stamped = JsonObject(settings + ("app" to JsonPrimitive("AAPS"))) - val response = api.createSetting(JsonParser.parseString(stamped.toString()).asJsonObject) + val response = api.createSetting(stamped) if (response.isSuccessful) { if (response.code() == 200 || response.code() == 201) { return@callWrapper CreateUpdateResponse( @@ -632,7 +631,7 @@ class NSAndroidClientImpl( override suspend fun patchSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { - val response = api.patchSetting(JsonParser.parseString(settings.toString()).asJsonObject, identifier) + val response = api.patchSetting(settings, identifier) if (response.code() == 404) { return@callWrapper CreateUpdateResponse( response = 404, @@ -661,7 +660,7 @@ class NSAndroidClientImpl( override suspend fun updateSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { - val response = api.updateSetting(JsonParser.parseString(settings.toString()).asJsonObject, identifier) + val response = api.updateSetting(settings, identifier) if (response.isSuccessful) { return@callWrapper CreateUpdateResponse( response = response.code(), diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt new file mode 100644 index 000000000000..68282d19423b --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt @@ -0,0 +1,63 @@ +package app.aaps.core.nssdk + +import kotlinx.serialization.json.Json + +/** + * The one JSON configuration the Nightscout wire layer uses, for both Retrofit and the string + * mappers, so they cannot drift apart. + * + * Each flag is here to match what Gson did before, not because it is a good default in the + * abstract. `GsonTypeCoercionTest` pins the old behaviour and must keep passing. + * + * - **`ignoreUnknownKeys`** - Nightscout documents carry fields this version has never seen, from + * other uploaders and newer AAPS builds. Gson dropped them silently; without this, kotlinx throws. + * + * - **`explicitNulls = false`** - on the way out, a null field is omitted rather than written as + * `"field": null`. Gson omits nulls by default, and NS validation rejects some explicit nulls. + * + * - **`isLenient`** - the one that needs justifying. It exists for a single measured difference: + * a bare **number arriving in a `String` field**. Strict kotlinx already accepts a quoted number + * in a `Long` / `Double` / `Int` field and `"true"` in a `Boolean` field, so those need nothing. + * The gap is real data, not a hypothetical: [app.aaps.core.nssdk.remotemodel.RemoteTreatment] + * declares `created_at` as `String?` and its own comment records that servers send it "with string + * others with long". + * + * The cost is that some malformed JSON which throws today would parse instead. That is the safer + * direction here: the socket.io listener that feeds this + * (`NSClientV3Service.onDataCreateUpdate`) has no try/catch, so a throw escapes onto the socket + * callback thread and loses the record. `RealNightscoutTreatmentTest` pins which malformed inputs + * still throw. + * + * - **`encodeDefaults = true`** - required for backward compatibility, and the easiest one to get + * wrong, because kotlinx's default is the opposite of Gson's. + * + * Gson writes every non-null field. kotlinx omits any field whose value equals its default, so + * `isReadOnly = false`, `duration = 0` and every one of the nullable fields that were given a + * `= null` default would simply stop being written. A document AAPS uploads has to stay readable + * by older AAPS versions and by the Nightscout server's own validation, neither of which is + * updated at the same time as this app - and NS rejects some documents outright for a missing + * field (`"Bad or missing utcOffset field"`). Absent is not the same as false. + * + * `NsSdkWireFormatTest` compares what goes onto the wire, field by field. + * + * - **`coerceInputValues = true`** - forward compatibility with senders that are newer than this + * app, and the other flag that is not optional. + * + * An **unknown enum value** throws in kotlinx but was silently mapped to `null` by Gson. That is + * not a corner case: `eventType` is an enum, Nightscout is written to by many uploaders, and a + * newer AAPS adding one event type would otherwise make every older client throw on that record - + * inside a socket.io listener with no try/catch, so the record is lost and the exception escapes + * onto the callback thread. Coercing to the default (`null`) keeps the old, tolerant behaviour: + * the treatment is simply not recognised and is skipped, which is what already happens for an + * absent `eventType`. + * + * Found by `RealNightscoutTreatmentTest`, which parses an `"eventType":"Something New"` record for + * exactly this reason. + */ +internal val nsSdkJson = Json { + ignoreUnknownKeys = true + explicitNulls = false + isLenient = true + encodeDefaults = true + coerceInputValues = true +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt index 1c659cdd9f5a..5964bbffaea9 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt @@ -1,42 +1,44 @@ package app.aaps.core.nssdk.localmodel.treatment -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Suppress("unused") +@Serializable enum class EventType(val text: String) { - @SerializedName("Site Change") CANNULA_CHANGE("Site Change"), - @SerializedName("Insulin Change") INSULIN_CHANGE("Insulin Change"), - @SerializedName("Pump Battery Change") PUMP_BATTERY_CHANGE("Pump Battery Change"), - @SerializedName("Sensor Change") SENSOR_CHANGE("Sensor Change"), - @SerializedName("Sensor Start") SENSOR_STARTED("Sensor Start"), - @SerializedName("Sensor Stop") SENSOR_STOPPED("Sensor Stop"), - @SerializedName("BG Check") FINGER_STICK_BG_VALUE("BG Check"), - @SerializedName("Exercise") EXERCISE("Exercise"), - @SerializedName("Announcement") ANNOUNCEMENT("Announcement"), - @SerializedName("SettingsExport") SETTINGS_EXPORT("Settings Export"), - @SerializedName("Question") QUESTION("Question"), - @SerializedName("Note") NOTE("Note"), - @SerializedName("OpenAPS Offline") APS_OFFLINE("OpenAPS Offline"), - @SerializedName("D.A.D. Alert") DAD_ALERT("D.A.D. Alert"), - @SerializedName("Mbg") NS_MBG("Mbg"), + @SerialName("Site Change") CANNULA_CHANGE("Site Change"), + @SerialName("Insulin Change") INSULIN_CHANGE("Insulin Change"), + @SerialName("Pump Battery Change") PUMP_BATTERY_CHANGE("Pump Battery Change"), + @SerialName("Sensor Change") SENSOR_CHANGE("Sensor Change"), + @SerialName("Sensor Start") SENSOR_STARTED("Sensor Start"), + @SerialName("Sensor Stop") SENSOR_STOPPED("Sensor Stop"), + @SerialName("BG Check") FINGER_STICK_BG_VALUE("BG Check"), + @SerialName("Exercise") EXERCISE("Exercise"), + @SerialName("Announcement") ANNOUNCEMENT("Announcement"), + @SerialName("SettingsExport") SETTINGS_EXPORT("Settings Export"), + @SerialName("Question") QUESTION("Question"), + @SerialName("Note") NOTE("Note"), + @SerialName("OpenAPS Offline") APS_OFFLINE("OpenAPS Offline"), + @SerialName("D.A.D. Alert") DAD_ALERT("D.A.D. Alert"), + @SerialName("Mbg") NS_MBG("Mbg"), // Used but not as a Therapy Event (use constants only) - @SerializedName("Carb Correction") CARBS_CORRECTION("Carb Correction"), - @SerializedName("Bolus Wizard") BOLUS_WIZARD("Bolus Wizard"), - @SerializedName("Correction Bolus") CORRECTION_BOLUS("Correction Bolus"), - @SerializedName("Meal Bolus") MEAL_BOLUS("Meal Bolus"), - @SerializedName("Combo Bolus") COMBO_BOLUS("Combo Bolus"), - @SerializedName("Temporary Target") TEMPORARY_TARGET("Temporary Target"), - @SerializedName("Temporary Target Cancel") TEMPORARY_TARGET_CANCEL("Temporary Target Cancel"), - @SerializedName("Profile Switch") PROFILE_SWITCH("Profile Switch"), - @SerializedName("Snack Bolus") SNACK_BOLUS("Snack Bolus"), - @SerializedName("Temp Basal") TEMPORARY_BASAL("Temp Basal"), - @SerializedName("Temp Basal Start") TEMPORARY_BASAL_START("Temp Basal Start"), - @SerializedName("Temp Basal End") TEMPORARY_BASAL_END("Temp Basal End"), + @SerialName("Carb Correction") CARBS_CORRECTION("Carb Correction"), + @SerialName("Bolus Wizard") BOLUS_WIZARD("Bolus Wizard"), + @SerialName("Correction Bolus") CORRECTION_BOLUS("Correction Bolus"), + @SerialName("Meal Bolus") MEAL_BOLUS("Meal Bolus"), + @SerialName("Combo Bolus") COMBO_BOLUS("Combo Bolus"), + @SerialName("Temporary Target") TEMPORARY_TARGET("Temporary Target"), + @SerialName("Temporary Target Cancel") TEMPORARY_TARGET_CANCEL("Temporary Target Cancel"), + @SerialName("Profile Switch") PROFILE_SWITCH("Profile Switch"), + @SerialName("Snack Bolus") SNACK_BOLUS("Snack Bolus"), + @SerialName("Temp Basal") TEMPORARY_BASAL("Temp Basal"), + @SerialName("Temp Basal Start") TEMPORARY_BASAL_START("Temp Basal Start"), + @SerialName("Temp Basal End") TEMPORARY_BASAL_END("Temp Basal End"), - @SerializedName("") ERROR(""), - @SerializedName("") NONE(""); + @SerialName("") ERROR(""), + @SerialName("") NONE(""); companion object { fun fromString(text: String?) = entries.firstOrNull { it.text == text } ?: NONE diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt index fbd9e680aba8..937c9f9e3d06 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt @@ -33,10 +33,11 @@ data class NSTherapyEvent( var glucoseType: MeterType? = null, ) : NSTreatment { + @Serializable enum class MeterType(val text: String) { - @com.google.gson.annotations.SerializedName("Finger") FINGER("Finger"), - @com.google.gson.annotations.SerializedName("Sensor") SENSOR("Sensor"), - @com.google.gson.annotations.SerializedName("Manual") MANUAL("Manual") + @SerialName("Finger") FINGER("Finger"), + @SerialName("Sensor") SENSOR("Sensor"), + @SerialName("Manual") MANUAL("Manual") ; companion object { diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt index 77390eeffe13..89e2f03a6a40 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt @@ -1,16 +1,14 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.devicestatus.NSDeviceStatus +import app.aaps.core.nssdk.nsSdkJson import app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus -import com.google.gson.Gson -import com.google.gson.JsonParser -import kotlinx.serialization.json.Json fun NSDeviceStatus.convertToRemoteAndBack(): NSDeviceStatus = toRemoteDeviceStatus().toNSDeviceStatus() fun String.toNSDeviceStatus(): NSDeviceStatus = - Gson().fromJson(this, RemoteDeviceStatus::class.java).toNSDeviceStatus() + nsSdkJson.decodeFromString(RemoteDeviceStatus.serializer(), this).toNSDeviceStatus() internal fun RemoteDeviceStatus.toNSDeviceStatus(): NSDeviceStatus = NSDeviceStatus( @@ -44,6 +42,11 @@ internal fun NSDeviceStatus.toRemoteDeviceStatus(): RemoteDeviceStatus = openaps = openaps?.toRemoteDeviceStatusOpenAps() ) +// The schema-less subtrees (pump.extended, openaps.suggested / enacted / iob) used to be Gson trees +// on the remote side and kotlinx trees on the local side, so every one of them was rebuilt by +// printing it to text and parsing it back. Both sides are kotlinx now, so they are carried straight +// across - no copy, no reparse, and no chance of the round trip changing anything. + internal fun RemoteDeviceStatus.Pump.toNSDeviceStatusPump(): NSDeviceStatus.Pump = NSDeviceStatus.Pump( clock = clock, @@ -51,7 +54,7 @@ internal fun RemoteDeviceStatus.Pump.toNSDeviceStatusPump(): NSDeviceStatus.Pump reservoirDisplayOverride = reservoirDisplayOverride, battery = NSDeviceStatus.Pump.Battery(battery?.percent, battery?.voltage), status = NSDeviceStatus.Pump.Status(status?.status, status?.timestamp), - extended = extended?.let { Json.decodeFromString(it.toString()) } + extended = extended ) internal fun NSDeviceStatus.Pump.toRemoteDeviceStatusPump(): RemoteDeviceStatus.Pump = @@ -61,19 +64,19 @@ internal fun NSDeviceStatus.Pump.toRemoteDeviceStatusPump(): RemoteDeviceStatus. reservoirDisplayOverride = reservoirDisplayOverride, battery = RemoteDeviceStatus.Pump.Battery(battery?.percent, battery?.voltage), status = RemoteDeviceStatus.Pump.Status(status?.status, status?.timestamp), - extended = extended?.let { JsonParser.parseString(it.toString()).asJsonObject } + extended = extended ) internal fun RemoteDeviceStatus.OpenAps.toNSDeviceStatusOpenAps(): NSDeviceStatus.OpenAps = NSDeviceStatus.OpenAps( - suggested = suggested?.let { Json.decodeFromString(it.toString()) }, - enacted = enacted?.let { Json.decodeFromString(it.toString()) }, - iob = iob?.let { Json.decodeFromString(it.toString()) } + suggested = suggested, + enacted = enacted, + iob = iob ) internal fun NSDeviceStatus.OpenAps.toRemoteDeviceStatusOpenAps(): RemoteDeviceStatus.OpenAps = RemoteDeviceStatus.OpenAps( - suggested = suggested?.let { JsonParser.parseString(it.toString()).asJsonObject }, - enacted = enacted?.let { JsonParser.parseString(it.toString()).asJsonObject }, - iob = iob?.let { JsonParser.parseString(it.toString()).asJsonObject } + suggested = suggested, + enacted = enacted, + iob = iob ) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt index 052779742eb5..5dfba298f147 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt @@ -2,7 +2,7 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.food.NSFood import app.aaps.core.nssdk.remotemodel.RemoteFood -import com.google.gson.Gson +import app.aaps.core.nssdk.nsSdkJson /** * Convert to [RemoteFood] and back to [NSFood] @@ -14,7 +14,7 @@ fun NSFood.convertToRemoteAndBack(): NSFood? = toRemoteFood().toNSFood() fun String.toNSFood(): NSFood? = - Gson().fromJson(this, RemoteFood::class.java).toNSFood() + nsSdkJson.decodeFromString(RemoteFood.serializer(), this).toNSFood() internal fun RemoteFood.toNSFood(): NSFood? { when (type) { diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt index 67746c7f8ca5..3c01db59848f 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt @@ -3,10 +3,10 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.entry.NSMbgV3 import app.aaps.core.nssdk.localmodel.entry.NsUnits import app.aaps.core.nssdk.remotemodel.RemoteEntry -import com.google.gson.Gson +import app.aaps.core.nssdk.nsSdkJson fun String.toCalibrationMbg(): NSMbgV3? = - Gson().fromJson(this, RemoteEntry::class.java).toCalibrationMbg() + nsSdkJson.decodeFromString(RemoteEntry.serializer(), this).toCalibrationMbg() /** * Maps a NS `entries` document to an AAPS calibration mbg. diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt index f1facbedd7c1..f3ac4024fa19 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt @@ -4,13 +4,13 @@ import app.aaps.core.nssdk.localmodel.entry.Direction import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 import app.aaps.core.nssdk.localmodel.entry.NsUnits import app.aaps.core.nssdk.remotemodel.RemoteEntry -import com.google.gson.Gson +import app.aaps.core.nssdk.nsSdkJson fun NSSgvV3.convertToRemoteAndBack(): NSSgvV3? = toRemoteEntry().toSgv() fun String.toNSSgvV3(): NSSgvV3? = - Gson().fromJson(this, RemoteEntry::class.java).toSgv() + nsSdkJson.decodeFromString(RemoteEntry.serializer(), this).toSgv() internal fun RemoteEntry.toSgv(): NSSgvV3? { diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt index 22587961b9bc..8a4a36521748 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt @@ -14,7 +14,7 @@ import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryTarget import app.aaps.core.nssdk.localmodel.treatment.NSTherapyEvent import app.aaps.core.nssdk.localmodel.treatment.NSTreatment import app.aaps.core.nssdk.remotemodel.RemoteTreatment -import com.google.gson.Gson +import app.aaps.core.nssdk.nsSdkJson import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes @@ -28,7 +28,7 @@ fun NSTreatment.convertToRemoteAndBack(): NSTreatment? = toRemoteTreatment()?.toTreatment() fun String.toNSTreatment(): NSTreatment? = - Gson().fromJson(this, RemoteTreatment::class.java).toTreatment() + nsSdkJson.decodeFromString(RemoteTreatment.serializer(), this).toTreatment() internal fun RemoteTreatment.toTreatment(): NSTreatment? { val treatmentTimestamp = timestamp() diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt index ec9b5a35c171..31b7fc7d79af 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt @@ -1,17 +1,13 @@ package app.aaps.core.nssdk.networking import android.content.Context -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonDeserializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonObject +import app.aaps.core.nssdk.nsSdkJson import okhttp3.Cache +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.kotlinx.serialization.asConverterFactory import java.util.concurrent.TimeUnit internal object NetworkStackBuilder { @@ -49,7 +45,7 @@ internal object NetworkStackBuilder { logger = logger ) ) - .addConverterFactory(GsonConverterFactory.create(provideGson())) + .addConverterFactory(converterFactory) .build() private fun getAuthRefreshRetrofit( @@ -61,7 +57,7 @@ internal object NetworkStackBuilder { Retrofit.Builder() .baseUrl("https://$baseUrl/api/") .client(getAuthRefreshOkHttpClient(context = context, logging = logging, logger = logger)) - .addConverterFactory(GsonConverterFactory.create(provideGson())) + .addConverterFactory(converterFactory) .build() private fun getOkHttpClient( @@ -97,22 +93,10 @@ internal object NetworkStackBuilder { return build() } - /** - * Schema-less documents (profiles, settings) are carried as kotlinx [JsonObject] rather than - * being modelled, because AAPS does not own their shape. - * - * This used to build `org.json.JSONObject`, which is a JVM and Android API and cannot go to - * iOS. The bridge is the same as before - Gson tree to text, text to the target type - so the - * parsed result is unchanged. `asJsonObject` still throws for a non object, as it always did. - */ - private val deserializer: JsonDeserializer = - JsonDeserializer { json, _, _ -> - Json.parseToJsonElement(json.asJsonObject.toString()).jsonObject - } - - private fun provideGson(): Gson = GsonBuilder().also { - it.registerTypeAdapter(JsonObject::class.java, deserializer) - }.create() + // The schema-less documents (profiles, settings) are carried as kotlinx JsonObject. Gson needed a + // registered type adapter to build those; kotlinx reads JsonObject natively, so the adapter and + // the Gson instance behind it are gone. + private val converterFactory = nsSdkJson.asConverterFactory("application/json".toMediaType()) private const val OK_HTTP_CACHE_SIZE = 10L * 1024 * 1024 private const val OK_HTTP_READ_TIMEOUT = 60L * 1000 diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt index c027a1d21fa7..b3ba95b72c72 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt @@ -1,8 +1,5 @@ package app.aaps.core.nssdk.networking -// Gson still builds the request bodies (the converter switch is a separate step), while responses -// already come back as kotlinx JsonObject. Both simple names are `JsonObject`, so the Gson one is -// aliased - when the converter goes, the alias and its uses go with it. import app.aaps.core.nssdk.remotemodel.LastModified import app.aaps.core.nssdk.remotemodel.NSResponse import app.aaps.core.nssdk.remotemodel.RemoteCreateUpdateResponse @@ -21,7 +18,6 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query -import com.google.gson.JsonObject as GsonJsonObject /** * Created by adrian on 2019-12-23. @@ -104,7 +100,7 @@ internal interface NightscoutRemoteService { suspend fun getLastProfile(): Response>> @POST("v3/profile") - suspend fun createProfile(@Body profile: GsonJsonObject): Response + suspend fun createProfile(@Body profile: JsonObject): Response @GET("v3/settings/{identifier}") suspend fun getSetting(@Path("identifier") identifier: String): Response> @@ -116,14 +112,14 @@ internal interface NightscoutRemoteService { suspend fun searchSettings(@Query("limit") limit: Int = 100): Response>> @POST("v3/settings") - suspend fun createSetting(@Body settings: GsonJsonObject): Response + suspend fun createSetting(@Body settings: JsonObject): Response @PATCH("v3/settings/{identifier}") - suspend fun patchSetting(@Body settings: GsonJsonObject, @Path("identifier") identifier: String): Response + suspend fun patchSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response /** PUT (NS3 "UPDATE") = upsert. Replaces existing doc, or inserts if absent. */ @PUT("v3/settings/{identifier}") - suspend fun updateSetting(@Body settings: GsonJsonObject, @Path("identifier") identifier: String): Response + suspend fun updateSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response /** [permanent] = `null` → soft delete (tombstone); `true` → NS `?permanent=true` hard delete. */ @DELETE("v3/settings/{identifier}") diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt index c15f2722e28d..819268feb95f 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt @@ -1,6 +1,6 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** @@ -9,18 +9,18 @@ import kotlinx.serialization.Serializable **/ @Serializable data class LastModified( - @SerializedName("collections") val collections: Collections + @SerialName("collections") val collections: Collections ) { @Serializable data class Collections( - @SerializedName("devicestatus") var devicestatus: Long = 0, // devicestatus collection - @SerializedName("entries") var entries: Long = 0, // entries collection - @SerializedName("profile") var profile: Long = 0, // profile collection - @SerializedName("treatments") var treatments: Long = 0, // treatments collection - @SerializedName("foods") var foods: Long = 0, // foods collection - @SerializedName("settings") var settings: Long = 0 // settings collection + @SerialName("devicestatus") var devicestatus: Long = 0, // devicestatus collection + @SerialName("entries") var entries: Long = 0, // entries collection + @SerialName("profile") var profile: Long = 0, // profile collection + @SerialName("treatments") var treatments: Long = 0, // treatments collection + @SerialName("foods") var foods: Long = 0, // foods collection + @SerialName("settings") var settings: Long = 0 // settings collection ) fun set(colName: String, value: Long) { diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt index a50a27b27209..8189816cd5fa 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt @@ -1,3 +1,11 @@ package app.aaps.core.nssdk.remotemodel +import kotlinx.serialization.Serializable + +/** + * All three fields stay non-null and without defaults on purpose: this is the response to a + * successful token refresh, so a reply missing any of them is not something to carry on from. + * Gson would have left them null and failed later; kotlinx fails here, which is the better place. + */ +@Serializable internal data class RemoteAuthResponse(val token: String, val iat: Long, val exp: Long) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt index 44d1805ee794..0636531f1f87 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt @@ -1,59 +1,66 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.JsonObject -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** * DeviceStatus coming from uploader or AAPS * **/ +@Serializable internal data class RemoteDeviceStatus( - @SerializedName("app") var app: String? = null, - @SerializedName("identifier") + @SerialName("app") var app: String? = null, + @SerialName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. - @SerializedName("srvCreated") + @SerialName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. - @SerializedName("srvModified") + @SerialName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). - @SerializedName("created_at") + @SerialName("created_at") val createdAt: String? = null, // string or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerializedName("date") val date: Long? = null, // date as milliseconds - @SerializedName("uploaderBattery") val uploaderBattery: Int? = null,// integer($int64) - @SerializedName("isCharging") val isCharging: Boolean? = null, - @SerializedName("device") val device: String? = null, // "openaps://samsung SM-G970F" - - @SerializedName("uploader") val uploader: Uploader? = null, - @SerializedName("pump") val pump: Pump? = null, - @SerializedName("openaps") val openaps: OpenAps? = null + @SerialName("date") val date: Long? = null, // date as milliseconds + @SerialName("uploaderBattery") val uploaderBattery: Int? = null,// integer($int64) + @SerialName("isCharging") val isCharging: Boolean? = null, + @SerialName("device") val device: String? = null, // "openaps://samsung SM-G970F" + + @SerialName("uploader") val uploader: Uploader? = null, + @SerialName("pump") val pump: Pump? = null, + @SerialName("openaps") val openaps: OpenAps? = null ) { + @Serializable data class Pump( - @SerializedName("clock") val clock: String? = null, // timestamp in ISO - @SerializedName("reservoir") val reservoir: Double? = null, - @SerializedName("reservoir_display_override") val reservoirDisplayOverride: String? = null, - @SerializedName("battery") val battery: Battery? = null, - @SerializedName("status") val status: Status? = null, - @SerializedName("extended") val extended: JsonObject? = null // Gson, content depending on pump driver + @SerialName("clock") val clock: String? = null, // timestamp in ISO + @SerialName("reservoir") val reservoir: Double? = null, + @SerialName("reservoir_display_override") val reservoirDisplayOverride: String? = null, + @SerialName("battery") val battery: Battery? = null, + @SerialName("status") val status: Status? = null, + @SerialName("extended") val extended: JsonObject? = null // schema-less, content depends on the pump driver ) { + @Serializable data class Battery( - @SerializedName("percent") val percent: Int? = null, - @SerializedName("voltage") val voltage: Double? = null + @SerialName("percent") val percent: Int? = null, + @SerialName("voltage") val voltage: Double? = null ) + @Serializable data class Status( - @SerializedName("status") val status: String? = null, - @SerializedName("timestamp") val timestamp: String? = null + @SerialName("status") val status: String? = null, + @SerialName("timestamp") val timestamp: String? = null ) } + @Serializable data class OpenAps( - @SerializedName("suggested") val suggested: JsonObject? = null, // Gson - @SerializedName("enacted") val enacted: JsonObject? = null, // Gson - @SerializedName("iob") val iob: JsonObject? = null // Gson + @SerialName("suggested") val suggested: JsonObject? = null, + @SerialName("enacted") val enacted: JsonObject? = null, + @SerialName("iob") val iob: JsonObject? = null ) + @Serializable data class Uploader( - @SerializedName("battery") val battery: Int? = null + @SerialName("battery") val battery: Int? = null ) } diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt index d5064f7f6507..1d0eace198e7 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt @@ -1,6 +1,7 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /* * Depending on the type, different other fields are present. @@ -11,30 +12,36 @@ import com.google.gson.annotations.SerializedName * TODO: Find out all types with their optional and mandatory fields * * */ +@Serializable internal data class RemoteEntry( - @SerializedName("type") val type: String, // sgv, mbg, cal, etc; Bolus type NORMAL, SMB, PRIMING - @SerializedName("sgv") val sgv: Double? = null, // number The glucose reading. (only available for sgv types) - @SerializedName("dateString") val dateString: String? = null, - @SerializedName("date") var date: Long? = null, // required ? TODO: date and dateString are redundant - are both needed? how to handle inconsistency then? Only expose one to clients? - @SerializedName("device") val device: String? = null, // The device from which the data originated (including serial number of the device, if it is relevant and safe). - @SerializedName("direction") val direction: String? = null, // TODO: what implicit convention for the directions exists? - @SerializedName("identifier") val identifier: String? = null, - @SerializedName("srvModified") val srvModified: Long? = null, - @SerializedName("srvCreated") val srvCreated: Long? = null, + // Default on purpose, same reason as RemoteFood: `entries` is a shared multi-writer collection + // and Nightscout does not mark `type` required. Gson left it null and both SvgMapper and + // MbgMapper then skipped the record; kotlinx would treat it as mandatory and throw for the whole + // page, and LoadBgWorker only advances its cursor after a successful decode - so one such record + // would stop all glucose reception and re-request the same page forever. + @SerialName("type") val type: String = "", // sgv, mbg, cal, etc; Bolus type NORMAL, SMB, PRIMING + @SerialName("sgv") val sgv: Double? = null, // number The glucose reading. (only available for sgv types) + @SerialName("dateString") val dateString: String? = null, + @SerialName("date") var date: Long? = null, // required ? TODO: date and dateString are redundant - are both needed? how to handle inconsistency then? Only expose one to clients? + @SerialName("device") val device: String? = null, // The device from which the data originated (including serial number of the device, if it is relevant and safe). + @SerialName("direction") val direction: String? = null, // TODO: what implicit convention for the directions exists? + @SerialName("identifier") val identifier: String? = null, + @SerialName("srvModified") val srvModified: Long? = null, + @SerialName("srvCreated") val srvCreated: Long? = null, // Philoul Others fields below found in API v3 doc - @SerializedName("app") var app: String? = null, - @SerializedName("utcOffset") var utcOffset: Long? = null, // Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is + @SerialName("app") var app: String? = null, + @SerialName("utcOffset") var utcOffset: Long? = null, // Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is // automatically parsed from the date field. - @SerializedName("subject") val subject: String? = null, // Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. - @SerializedName("modifiedBy") val modifiedBy: String? = null, // Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. - @SerializedName("isValid") val isValid: Boolean? = null, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) - @SerializedName("isReadOnly") val isReadOnly: Boolean? = null, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerializedName("noise") val noise: Double? = null, // 0 or 1 found in the export, I don't know if other values possible ? - @SerializedName("filtered") val filtered: Double? = null, // The raw filtered value directly from CGM transmitter. (only available for sgv types) - @SerializedName("unfiltered") val unfiltered: Double? = null, // The raw unfiltered value directly from CGM transmitter. (only available for sgv types) - @SerializedName("units") val units: String? = null, // The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. - @SerializedName("mbg") val mbg: Double? = null, // Manual blood glucose reading (only available for mbg types). AAPS uses a marked mbg to carry a calibration fingerstick. + @SerialName("subject") val subject: String? = null, // Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. + @SerialName("modifiedBy") val modifiedBy: String? = null, // Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. + @SerialName("isValid") val isValid: Boolean? = null, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) + @SerialName("isReadOnly") val isReadOnly: Boolean? = null, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. + @SerialName("noise") val noise: Double? = null, // 0 or 1 found in the export, I don't know if other values possible ? + @SerialName("filtered") val filtered: Double? = null, // The raw filtered value directly from CGM transmitter. (only available for sgv types) + @SerialName("unfiltered") val unfiltered: Double? = null, // The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + @SerialName("units") val units: String? = null, // The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + @SerialName("mbg") val mbg: Double? = null, // Manual blood glucose reading (only available for mbg types). AAPS uses a marked mbg to carry a calibration fingerstick. // AAPS-specific calibration fields, carried on a marked `mbg` entry so a follower can re-fit the calibration curve. - @SerializedName("sensorMgdlAtPairing") val sensorMgdlAtPairing: Double? = null, // sensor value at the moment the fingerstick was entered - @SerializedName("isCalibration") val isCalibration: Boolean? = null // marker: true when this mbg is an AAPS calibration pair (vs a foreign manual BG) + @SerialName("sensorMgdlAtPairing") val sensorMgdlAtPairing: Double? = null, // sensor value at the moment the fingerstick was entered + @SerialName("isCalibration") val isCalibration: Boolean? = null // marker: true when this mbg is an AAPS calibration pair (vs a foreign manual BG) ) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt index 4970ade2823b..8dfeaafb526e 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt @@ -1,6 +1,7 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** * Depending on the type, different other fields are present. @@ -9,33 +10,42 @@ import com.google.gson.annotations.SerializedName * On upload a sanity check still needs to be done to verify that all mandatory fields for that type are there. * **/ +@Serializable internal data class RemoteFood( - @SerializedName("type") val type: String, // we are interesting in type "food" - @SerializedName("date") val date: Long? = null, - @SerializedName("name") val name: String, - @SerializedName("category") val category: String? = null, - @SerializedName("subcategory") val subcategory: String? = null, - @SerializedName("unit") val unit: String? = null, - @SerializedName("portion") val portion: Double, - @SerializedName("carbs") val carbs: Int, - @SerializedName("gi") val gi: Int? = null, - @SerializedName("energy") val energy: Int? = null, - @SerializedName("protein") val protein: Int? = null, - @SerializedName("fat") val fat: Int? = null, - @SerializedName("identifier") + // These four carry defaults on purpose. The Nightscout `food` collection is shared: besides food + // records it also holds "quickpick" documents written by the Nightscout food editor, which have + // no portion and no carbs. Gson filled a missing non-null field with null / 0 / 0.0 and the + // `else -> return null` branch in FoodMapper then dropped the document, which is what that + // branch is for. kotlinx instead treats a non-null field without a default as **mandatory** and + // throws - and `v3/food` is decoded as one list, so a single quickpick would lose the whole food + // database. The defaults below reproduce Gson's values exactly, so the old tolerant behaviour is + // restored. `encodeDefaults = true` means the upload format does not change. + @SerialName("type") val type: String = "", // we are interesting in type "food" + @SerialName("date") val date: Long? = null, + @SerialName("name") val name: String = "", + @SerialName("category") val category: String? = null, + @SerialName("subcategory") val subcategory: String? = null, + @SerialName("unit") val unit: String? = null, + @SerialName("portion") val portion: Double = 0.0, + @SerialName("carbs") val carbs: Int = 0, + @SerialName("gi") val gi: Int? = null, + @SerialName("energy") val energy: Int? = null, + @SerialName("protein") val protein: Int? = null, + @SerialName("fat") val fat: Int? = null, + @SerialName("identifier") val identifier: String?, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. - @SerializedName("isValid") + @SerialName("isValid") val isValid: Boolean?, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) - @SerializedName("isReadOnly") + @SerialName("isReadOnly") val isReadOnly: Boolean?, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerializedName("app") var app: String? = null, // Application or system in which the record was entered by human or device for the first time. - @SerializedName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe). - @SerializedName("srvCreated") + @SerialName("app") var app: String? = null, // Application or system in which the record was entered by human or device for the first time. + @SerialName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe). + @SerialName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. - @SerializedName("subject") + @SerialName("subject") val subject: String? = null, // string Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. - @SerializedName("srvModified") + @SerialName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). - @SerializedName("modifiedBy") + @SerialName("modifiedBy") val modifiedBy: String? = null // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. ) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt index 8b0540f7ff11..7effc011fb28 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt @@ -1,10 +1,21 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +/** + * Insulin configuration attached to a treatment. + * + * Defaults are here for the same reason as in [RemoteFood]: a non-null field without a default is + * **mandatory** for kotlinx, and this is the only strict nested object on the treatments feed. Only + * a present-but-partial `icfg` would throw today, and nothing writes one - but a treatments page is + * decoded in a single pass, so if a future version ever adds a fifth field, every older reader would + * lose the whole page rather than one record. The defaults cost nothing and remove that trap. + */ +@Serializable data class RemoteICfg( - @SerializedName("insulinLabel") val insulinLabel: String, - @SerializedName("insulinEndTime") val insulinEndTime: Long, - @SerializedName("insulinPeakTime") val insulinPeakTime: Long, - @SerializedName("concentration") val concentration: Double + @SerialName("insulinLabel") val insulinLabel: String = "", + @SerialName("insulinEndTime") val insulinEndTime: Long = 0, + @SerialName("insulinPeakTime") val insulinPeakTime: Long = 0, + @SerialName("concentration") val concentration: Double = 0.0 ) \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt index e63e9ade05a2..e46b3a385128 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt @@ -1,36 +1,42 @@ package app.aaps.core.nssdk.remotemodel -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -internal data class NSResponse(val result: T?) +@Serializable +internal data class NSResponse(val result: T? = null) +@Serializable internal data class RemoteStatusResponse( - @SerializedName("version") val version: String, - @SerializedName("apiVersion") val apiVersion: String, - @SerializedName("srvDate") val srvDate: Long, - @SerializedName("storage") val storage: RemoteStorage, - @SerializedName("apiPermissions") val apiPermissions: RemoteApiPermissions + @SerialName("version") val version: String, + @SerialName("apiVersion") val apiVersion: String, + @SerialName("srvDate") val srvDate: Long, + @SerialName("storage") val storage: RemoteStorage, + @SerialName("apiPermissions") val apiPermissions: RemoteApiPermissions ) +@Serializable internal data class RemoteStorage( - @SerializedName("storage") val storage: String, - @SerializedName("version") val version: String + @SerialName("storage") val storage: String, + @SerialName("version") val version: String ) +@Serializable internal data class RemoteCreateUpdateResponse( - @SerializedName("identifier") val identifier: String? = null, - @SerializedName("isDeduplication") val isDeduplication: Boolean? = null, - @SerializedName("deduplicatedIdentifier") val deduplicatedIdentifier: String? = null, - @SerializedName("lastModified") val lastModified: Long? = null + @SerialName("identifier") val identifier: String? = null, + @SerialName("isDeduplication") val isDeduplication: Boolean? = null, + @SerialName("deduplicatedIdentifier") val deduplicatedIdentifier: String? = null, + @SerialName("lastModified") val lastModified: Long? = null ) +@Serializable internal data class RemoteApiPermissions( - @SerializedName("devicestatus") val deviceStatus: RemoteApiPermission, - @SerializedName("entries") val entries: RemoteApiPermission, - @SerializedName("food") val food: RemoteApiPermission, - @SerializedName("profile") val profile: RemoteApiPermission, - @SerializedName("settings") val settings: RemoteApiPermission, - @SerializedName("treatments") val treatments: RemoteApiPermission + @SerialName("devicestatus") val deviceStatus: RemoteApiPermission, + @SerialName("entries") val entries: RemoteApiPermission, + @SerialName("food") val food: RemoteApiPermission, + @SerialName("profile") val profile: RemoteApiPermission, + @SerialName("settings") val settings: RemoteApiPermission, + @SerialName("treatments") val treatments: RemoteApiPermission ) internal typealias RemoteApiPermission = String diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt index 7d1c9f5686c0..4812c58dceed 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt @@ -1,7 +1,8 @@ package app.aaps.core.nssdk.remotemodel import app.aaps.core.nssdk.localmodel.treatment.EventType -import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import org.joda.time.DateTime import org.joda.time.format.ISODateTimeFormat @@ -14,78 +15,79 @@ import org.joda.time.format.ISODateTimeFormat * TODO: Find out all types with their optional and mandatory fields * * */ +@Serializable internal data class RemoteTreatment( - @SerializedName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. - @SerializedName("date") var date: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') - @SerializedName("mills") val mills: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix - @SerializedName("timestamp") val timestamp: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') - @SerializedName("created_at") val created_at: String? = null, // integer($int64) or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerializedName("utcOffset") var utcOffset: Long? = null, // integer Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the date field. - @SerializedName("app") var app : String? = null, // Application or system in which the record was entered by human or device for the first time. - @SerializedName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe). - @SerializedName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. - @SerializedName("subject") val subject: String? = null, // string Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. - @SerializedName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). - @SerializedName("modifiedBy") val modifiedBy: String? = null, // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. - @SerializedName("isValid") val isValid: Boolean? = null, // boolean A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) - @SerializedName("isReadOnly") val isReadOnly: Boolean? = null, // boolean A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerializedName("eventType") val eventType: EventType? = null, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" - @SerializedName("glucose") val glucose: Double? = null, // double Current glucose - @SerializedName("glucoseType") val glucoseType: String? = null, // string example: "Sensor", "Finger", "Manual" - @SerializedName("units") val units: String? = null, // string The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. - @SerializedName("carbs") val carbs: Double? = null, // number... Amount of carbs given. - @SerializedName("protein") val protein: Int? = null, // number... Amount of protein given. - @SerializedName("fat") val fat: Int? = null, // number... Amount of fat given. - @SerializedName("insulin") val insulin: Double? = null, // number... Amount of insulin, if any. + @SerialName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. + @SerialName("date") var date: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + @SerialName("mills") val mills: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix + @SerialName("timestamp") val timestamp: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + @SerialName("created_at") val created_at: String? = null, // integer($int64) or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... + @SerialName("utcOffset") var utcOffset: Long? = null, // integer Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the date field. + @SerialName("app") var app : String? = null, // Application or system in which the record was entered by human or device for the first time. + @SerialName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe). + @SerialName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + @SerialName("subject") val subject: String? = null, // string Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. + @SerialName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + @SerialName("modifiedBy") val modifiedBy: String? = null, // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. + @SerialName("isValid") val isValid: Boolean? = null, // boolean A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) + @SerialName("isReadOnly") val isReadOnly: Boolean? = null, // boolean A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. + @SerialName("eventType") val eventType: EventType? = null, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" + @SerialName("glucose") val glucose: Double? = null, // double Current glucose + @SerialName("glucoseType") val glucoseType: String? = null, // string example: "Sensor", "Finger", "Manual" + @SerialName("units") val units: String? = null, // string The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + @SerialName("carbs") val carbs: Double? = null, // number... Amount of carbs given. + @SerialName("protein") val protein: Int? = null, // number... Amount of protein given. + @SerialName("fat") val fat: Int? = null, // number... Amount of fat given. + @SerialName("insulin") val insulin: Double? = null, // number... Amount of insulin, if any. /** Duration in minutes */ - @SerializedName("duration") val duration: Long? = null, // number... Duration in minutes. + @SerialName("duration") val duration: Long? = null, // number... Duration in minutes. /** Duration in milliseconds */ - @SerializedName("durationInMilliseconds") val durationInMilliseconds: Long? = null, // number... Duration in milliseconds. - @SerializedName("preBolus") val preBolus: Int? = null, // number... How many minutes the bolus was given before the meal started. - @SerializedName("splitNow") val splitNow: Int? = null, // number... Immediate part of combo bolus (in percent). - @SerializedName("splitExt") val splitExt: Int? = null, // number... Extended part of combo bolus (in percent). - @SerializedName("percent") val percent: Double? = null, // number... Eventual basal change in percent. - @SerializedName("absolute") val absolute: Double? = null, // number... Eventual basal change in absolute value (insulin units per hour). - @SerializedName("targetTop") val targetTop: Double? = null, // number... Top limit of temporary target. - @SerializedName("targetBottom") val targetBottom: Double? = null, // number... Bottom limit of temporary target. - @SerializedName("profile") val profile: String? = null, // string Name of the profile to which the pump has been switched. - @SerializedName("reason") val reason: String? = null, // string For example the reason why the profile has been switched or why the temporary target has been set. - @SerializedName("mode") val mode: String? = null, // string RunningMode - @SerializedName("location") val location: String? = null, // string Location for site management defined in TE.Location - @SerializedName("arrow") val arrow: String? = null, // string Arrow for site management defined in TE.Arrow - @SerializedName("autoForced") val autoForced: Boolean? = null, // boolean RunningMode - @SerializedName("reasons") val reasons: String? = null, // string RunningMode - @SerializedName("notes") val notes: String? = null, // string Description/notes of treatment. - @SerializedName("enteredBy") val enteredBy: String? = null, // string Who entered the treatment. + @SerialName("durationInMilliseconds") val durationInMilliseconds: Long? = null, // number... Duration in milliseconds. + @SerialName("preBolus") val preBolus: Int? = null, // number... How many minutes the bolus was given before the meal started. + @SerialName("splitNow") val splitNow: Int? = null, // number... Immediate part of combo bolus (in percent). + @SerialName("splitExt") val splitExt: Int? = null, // number... Extended part of combo bolus (in percent). + @SerialName("percent") val percent: Double? = null, // number... Eventual basal change in percent. + @SerialName("absolute") val absolute: Double? = null, // number... Eventual basal change in absolute value (insulin units per hour). + @SerialName("targetTop") val targetTop: Double? = null, // number... Top limit of temporary target. + @SerialName("targetBottom") val targetBottom: Double? = null, // number... Bottom limit of temporary target. + @SerialName("profile") val profile: String? = null, // string Name of the profile to which the pump has been switched. + @SerialName("reason") val reason: String? = null, // string For example the reason why the profile has been switched or why the temporary target has been set. + @SerialName("mode") val mode: String? = null, // string RunningMode + @SerialName("location") val location: String? = null, // string Location for site management defined in TE.Location + @SerialName("arrow") val arrow: String? = null, // string Arrow for site management defined in TE.Arrow + @SerialName("autoForced") val autoForced: Boolean? = null, // boolean RunningMode + @SerialName("reasons") val reasons: String? = null, // string RunningMode + @SerialName("notes") val notes: String? = null, // string Description/notes of treatment. + @SerialName("enteredBy") val enteredBy: String? = null, // string Who entered the treatment. - @SerializedName("endId") val endId: Long? = null, // long id of record which ended this - @SerializedName("pumpId") val pumpId: Long? = null, // long or "Meal Bolus", "Correction Bolus", "Combo Bolus" ex 4102 not sure if long or int - @SerializedName("pumpType") val pumpType: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus" ex "ACCU_CHEK_INSIGHT_BLUETOOTH", - @SerializedName("pumpSerial") val pumpSerial: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus" "33013206", + @SerialName("endId") val endId: Long? = null, // long id of record which ended this + @SerialName("pumpId") val pumpId: Long? = null, // long or "Meal Bolus", "Correction Bolus", "Combo Bolus" ex 4102 not sure if long or int + @SerialName("pumpType") val pumpType: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus" ex "ACCU_CHEK_INSIGHT_BLUETOOTH", + @SerialName("pumpSerial") val pumpSerial: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus" "33013206", // other fields found in examples but not in documentation - @SerializedName("profileJson") val profileJson: String? = null, // string "Profile Switch" ex json toString "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\", + @SerialName("profileJson") val profileJson: String? = null, // string "Profile Switch" ex json toString "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\", // \"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":60},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":60},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":61.33333333333333},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":65.33333333333333},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":69.33333333333333},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":73.33333333333333},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":72},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":68},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":65.33333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":65.33333333333333}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":5.7333333333333325},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":7.333333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":6.666666666666666}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.5249999999999999},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.585},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.6375},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.5625},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.4575},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.5175},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.48},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.51},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.48750000000000004},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.48},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.48750000000000004},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.5025000000000001},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.5549999999999999},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.5700000000000001},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.5700000000000001},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.5775},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.51},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.54},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.48750000000000004},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.5249999999999999},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.46499999999999997},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.46499999999999997},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.43499999999999994},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.41250000000000003}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", - @SerializedName("originalProfileName") val originalProfileName: String? = null, // string "Effective Profile Switch" - @SerializedName("originalCustomizedName") val originalCustomizedName: String? = null, // string "Effective Profile Switch" - @SerializedName("originalTimeshift") val originalTimeshift: Long? = null, // long "Effective Profile Switch" - @SerializedName("originalPercentage") val originalPercentage: Int? = null, // int "Effective Profile Switch" - @SerializedName("originalDuration") val originalDuration: Long? = null, // long "Effective Profile Switch", RunningMode - @SerializedName("originalEnd") val originalEnd: Long? = null, // long "Effective Profile Switch" - @SerializedName("icfg") val iCfg: RemoteICfg? = null, // long "Effective Profile Switch" + @SerialName("originalProfileName") val originalProfileName: String? = null, // string "Effective Profile Switch" + @SerialName("originalCustomizedName") val originalCustomizedName: String? = null, // string "Effective Profile Switch" + @SerialName("originalTimeshift") val originalTimeshift: Long? = null, // long "Effective Profile Switch" + @SerialName("originalPercentage") val originalPercentage: Int? = null, // int "Effective Profile Switch" + @SerialName("originalDuration") val originalDuration: Long? = null, // long "Effective Profile Switch", RunningMode + @SerialName("originalEnd") val originalEnd: Long? = null, // long "Effective Profile Switch" + @SerialName("icfg") val iCfg: RemoteICfg? = null, // long "Effective Profile Switch" - @SerializedName("bolusCalculatorResult") val bolusCalculatorResult: String? = null, // string "Bolus Wizard" json toString ex "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", - @SerializedName("type") val type: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus", "Temp Basal" type of bolus "NORMAL", "SMB", "FAKE_EXTENDED" - @SerializedName("isSMB") val isSMB: Boolean? = null, // boolean "Meal Bolus", "Correction Bolus", "Combo Bolus" - @SerializedName("enteredinsulin") val enteredinsulin: Double? = null, // number... "Combo Bolus" insulin is missing only enteredinsulin field found - @SerializedName("relative") val relative: Double? = null, // number... "Combo Bolus", "extendedEmulated" (not in doc see below) - @SerializedName("isEmulatingTempBasal") val isEmulatingTempBasal: Boolean? = null, // boolean "Combo Bolus", "extendedEmulated" (not in doc see below) - @SerializedName("isAnnouncement") val isAnnouncement: Boolean? = null, // boolean "Announcement" - @SerializedName("rate") val rate: Double? = null, // Double "Temp Basal" absolute rate (could be calculated with percent and profile information...) - @SerializedName("extendedEmulated") var extendedEmulated: RemoteTreatment? = null, // Gson of emulated EB - @SerializedName("timeshift") val timeshift: Long? = null, // integer "Profile Switch" - @SerializedName("percentage") val percentage: Int? = null, // integer "Profile Switch" - @SerializedName("isBasalInsulin") val isBasalInsulin: Boolean? = null // boolean "Bolus" + @SerialName("bolusCalculatorResult") val bolusCalculatorResult: String? = null, // string "Bolus Wizard" json toString ex "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", + @SerialName("type") val type: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus", "Temp Basal" type of bolus "NORMAL", "SMB", "FAKE_EXTENDED" + @SerialName("isSMB") val isSMB: Boolean? = null, // boolean "Meal Bolus", "Correction Bolus", "Combo Bolus" + @SerialName("enteredinsulin") val enteredinsulin: Double? = null, // number... "Combo Bolus" insulin is missing only enteredinsulin field found + @SerialName("relative") val relative: Double? = null, // number... "Combo Bolus", "extendedEmulated" (not in doc see below) + @SerialName("isEmulatingTempBasal") val isEmulatingTempBasal: Boolean? = null, // boolean "Combo Bolus", "extendedEmulated" (not in doc see below) + @SerialName("isAnnouncement") val isAnnouncement: Boolean? = null, // boolean "Announcement" + @SerialName("rate") val rate: Double? = null, // Double "Temp Basal" absolute rate (could be calculated with percent and profile information...) + @SerialName("extendedEmulated") var extendedEmulated: RemoteTreatment? = null, // Gson of emulated EB + @SerialName("timeshift") val timeshift: Long? = null, // integer "Profile Switch" + @SerialName("percentage") val percentage: Int? = null, // integer "Profile Switch" + @SerialName("isBasalInsulin") val isBasalInsulin: Boolean? = null // boolean "Bolus" ) { fun timestamp(): Long { diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt new file mode 100644 index 000000000000..52e37ad24f09 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt @@ -0,0 +1,151 @@ +package app.aaps.core.nssdk + +import app.aaps.core.nssdk.localmodel.treatment.EventType +import app.aaps.core.nssdk.remotemodel.LastModified +import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.jupiter.api.Test + +/** + * Pins what AAPS puts **on the wire**, not what it reads off it. + * + * Backward compatibility depends on this side. An AAPS instance uploads documents that are read by + * Nightscout's own validation, by other uploaders, and by AAPS clients running an older version - + * none of which update at the same time as this app. A field that silently stops being written is + * not a local problem, it is a problem on every reader that has not been updated. + * + * The specific hazard is that kotlinx's default is the **opposite** of Gson's. Gson writes every + * non-null field. kotlinx omits any field whose value equals its default unless `encodeDefaults` is + * on, so `0`, `false` and every field that was given a `= null` default would quietly disappear. + * See [app.aaps.core.nssdk.nsSdkJson]. + */ +class NsSdkWireFormatTest { + + private fun encoded(treatment: RemoteTreatment) = + Json.parseToJsonElement(nsSdkJson.encodeToString(RemoteTreatment.serializer(), treatment)).jsonObject + + // ---------------------------------------------------------------- field names + + /** The wire names are the `@SerialName` ones, not the Kotlin property names. */ + @Test + fun `wire names are used, not property names`() { + val json = encoded( + RemoteTreatment( + eventType = EventType.CORRECTION_BOLUS, + date = 1785992179555, + created_at = "2026-08-06T04:56:19.555Z", + utcOffset = 120, + insulin = 0.25, + isValid = true, + isReadOnly = false + ) + ) + + assertThat(json.keys).containsAtLeast("eventType", "date", "created_at", "utcOffset", "insulin", "isValid", "isReadOnly") + // the Kotlin name must not leak onto the wire + assertThat(json.keys).doesNotContain("createdAt") + } + + // ---------------------------------------------------------------- values that equal a default + + /** + * The regression this file exists for. `false` and `0` are real values a reader needs, and both + * are also the natural default for their type - exactly what kotlinx would drop. + */ + @Test + fun `false and zero are written, not omitted`() { + val json = encoded( + RemoteTreatment( + eventType = EventType.TEMPORARY_TARGET, + date = 1785994614220, + isValid = true, + isReadOnly = false, // false, not absent + utcOffset = 0, // zero, not absent + duration = 0 // zero, not absent + ) + ) + + assertThat(json.keys).containsAtLeast("isReadOnly", "utcOffset", "duration") + assertThat(json["isReadOnly"]?.jsonPrimitive?.content).isEqualTo("false") + assertThat(json["utcOffset"]?.jsonPrimitive?.content).isEqualTo("0") + assertThat(json["duration"]?.jsonPrimitive?.content).isEqualTo("0") + } + + /** Same hazard on a model whose defaults are non-null: every counter must be written. */ + @Test + fun `zero collection timestamps are written`() { + val json = Json.parseToJsonElement( + nsSdkJson.encodeToString(LastModified.serializer(), LastModified(LastModified.Collections())) + ).jsonObject["collections"]!!.jsonObject + + assertThat(json.keys).containsExactly("devicestatus", "entries", "profile", "treatments", "foods", "settings") + assertThat(json["devicestatus"]?.jsonPrimitive?.content).isEqualTo("0") + assertThat(json["treatments"]?.jsonPrimitive?.content).isEqualTo("0") + } + + // ---------------------------------------------------------------- nulls stay off the wire + + /** + * The other half of matching Gson: a null field is omitted rather than written as + * `"field": null`. Nightscout validation rejects some explicit nulls, so this is not cosmetic. + */ + @Test + fun `null fields are omitted rather than written as null`() { + val json = encoded(RemoteTreatment(eventType = EventType.CARBS_CORRECTION, date = 1785987360000, carbs = 4.0)) + + assertThat(json.keys).containsAtLeast("eventType", "date", "carbs") + assertThat(json.keys).doesNotContain("insulin") + assertThat(json.keys).doesNotContain("notes") + assertThat(json.keys).doesNotContain("pumpSerial") + } + + // ---------------------------------------------------------------- enums + + /** Enums go out as their Nightscout text, not as the Kotlin constant name. */ + @Test + fun `event types are written as their Nightscout text`() { + assertThat(encoded(RemoteTreatment(eventType = EventType.CORRECTION_BOLUS))["eventType"]?.jsonPrimitive?.content) + .isEqualTo("Correction Bolus") + assertThat(encoded(RemoteTreatment(eventType = EventType.TEMPORARY_TARGET))["eventType"]?.jsonPrimitive?.content) + .isEqualTo("Temporary Target") + assertThat(encoded(RemoteTreatment(eventType = EventType.TEMPORARY_BASAL))["eventType"]?.jsonPrimitive?.content) + .isEqualTo("Temp Basal") + } + + // ---------------------------------------------------------------- numbers + + /** A whole number in a Double field keeps its decimal point, as Gson wrote it. */ + @Test + fun `numbers keep their form`() { + val json = encoded(RemoteTreatment(eventType = EventType.CARBS_CORRECTION, carbs = 4.0, insulin = 0.25)) + + assertThat(json["carbs"]?.jsonPrimitive?.content).isEqualTo("4.0") + assertThat(json["insulin"]?.jsonPrimitive?.content).isEqualTo("0.25") + } + + // ---------------------------------------------------------------- round trip against a real record + + /** + * A real uploaded record, parsed and written back out. Every field the wire model knows about has + * to come back with the same value - this is what an older reader will be looking at. + */ + @Test + fun `a real record survives being read and written again`() { + val original = """ + {"app":"AAPS","date":1785992179555,"eventType":"Correction Bolus","insulin":0.25, + "isBasalInsulin":false,"isReadOnly":false,"isValid":true,"pumpId":1785992181495, + "pumpSerial":"TESTSERIAL","pumpType":"DANA_RS","type":"SMB","utcOffset":120, + "created_at":"2026-08-06T04:56:19.555Z","identifier":"1bebd84b-8d61-58b2-a967-1b34a1e8c4d4"} + """.trimIndent() + + val parsed = nsSdkJson.decodeFromString(RemoteTreatment.serializer(), original) + val written = encoded(parsed) + val before = Json.parseToJsonElement(original).jsonObject + + for (key in before.keys) + assertThat(written[key]).isEqualTo(before[key]) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt index fe8fa18b8687..b17922b4e71b 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt @@ -1,28 +1,22 @@ package app.aaps.core.nssdk.mapper import com.google.common.truth.Truth.assertThat -import com.google.gson.JsonSyntaxException import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows /** * Device status carries **schema-less** JSON: `pump.extended`, `openaps.suggested`, `openaps.enacted` * and `openaps.iob` hold whatever the pump driver and the APS algorithm put there. AAPS does not * know their shape, it only has to carry them through without losing anything. * - * Today the wire model [app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus] holds those subtrees as - * **Gson** `JsonObject`, while the local model - * [app.aaps.core.nssdk.localmodel.devicestatus.NSDeviceStatus] already holds them as **kotlinx** - * `JsonObject`. `DeviceStatusMapper` bridges the two by turning a tree into text and parsing it - * again, in both directions. - * - * These tests assert on **parsed structure**, never on `toString()`. The existing - * `DeviceStatusExtensionKtTest` compares string form, which would fail after a move to kotlinx even - * if every value were preserved, because the two libraries do not print identically. + * These tests were written while the wire model still held those subtrees as **Gson** `JsonObject` + * and the local model held them as **kotlinx** `JsonObject`, so `DeviceStatusMapper` rebuilt every + * subtree by printing it to text and parsing it back. Both sides are kotlinx now and the subtrees + * are passed straight through, which is why they still pass unchanged - that was the point of + * asserting on parsed structure rather than on `toString()`. * * The last test is the important one: since the schema is unknown, a rewrite that quietly drops keys * it does not recognise would pass every other assertion here. @@ -172,23 +166,25 @@ class DeviceStatusMapperTest { } /** - * An **explicit** `null` is not the same as an absent key here, and it throws. - * - * The field is declared `JsonObject?`, but Gson's adapter for the concrete `JsonObject` type - * rejects `JsonNull` instead of mapping it to Kotlin `null`. An absent key is fine, a written - * `null` is not. + * An **explicit** `null` subtree now decodes to Kotlin `null` instead of throwing. * - * Pinned, not fixed - the caller `NSClientV3Service.onDataCreateUpdate` is a socket.io listener - * with no try/catch, so this escapes onto the socket callback thread. Worth noting that AAPS - * itself never writes `null` here, but nothing stops another uploader from doing so. + * This is the deliberate behaviour change the previous version of this test predicted. Under + * Gson the field was declared `JsonObject?`, but Gson's adapter for the concrete `JsonObject` + * type rejected `JsonNull` rather than mapping it to `null`, so an absent key was fine and a + * written `null` threw. kotlinx maps it to `null`, which is what the declared type says. * - * kotlinx.serialization would accept it and give `null`, so a migration would **change** this. - * If that is wanted, change this test on purpose. + * Changed on purpose, and it is an improvement: the caller + * `NSClientV3Service.onDataCreateUpdate` is a socket.io listener with no try/catch, so the old + * exception escaped onto the socket callback thread and lost the whole device status. AAPS never + * writes `null` here, but nothing stops another uploader from doing so. */ @Test - fun `an explicit null subtree throws - current behaviour, pinned`() { + fun `an explicit null subtree decodes to null - changed on purpose`() { val json = """{"app":"AAPS","date":1,"openaps":{"suggested":{},"enacted":null}}""" - assertThrows { json.toNSDeviceStatus() } + + val openaps = json.toNSDeviceStatus().openaps + assertThat(openaps?.suggested).isNotNull() + assertThat(openaps?.enacted).isNull() } /** diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt new file mode 100644 index 000000000000..110f8b2503f4 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt @@ -0,0 +1,137 @@ +package app.aaps.core.nssdk.mapper + +import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.NSResponse +import app.aaps.core.nssdk.remotemodel.RemoteEntry +import app.aaps.core.nssdk.remotemodel.RemoteFood +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.builtins.ListSerializer +import org.junit.jupiter.api.Test + +/** + * Regression tests for the one thing the Gson to kotlinx move changed that nothing was watching. + * + * Under Gson, a field declared non-null but **without a default** was quietly filled with + * `null` / `0` / `0.0` when the server did not send it, because Gson builds objects through + * `Unsafe.allocateInstance` and never calls the constructor. kotlinx treats exactly the same + * declaration as **mandatory** and throws. + * + * That difference is not per record. `v3/food` and `v3/entries` are decoded as one list, so a single + * unparseable document takes the whole page with it - and the workers only advance their cursor + * after a successful decode, so the same page is requested again forever. + * + * Neither `coerceInputValues` nor `explicitNulls` helps: both only apply to properties that already + * have a default. The fix was to give the affected fields the same values Gson used to leave behind. + * + * These collections are shared and multi-writer - Nightscout's own food editor, xDrip, Loop and + * other uploaders all write to them - so "AAPS always sends this field" is not a safe assumption. + */ +class FoodAndEntryToleranceTest { + + // ---------------------------------------------------------------- food + + /** + * A "quickpick" document. Nightscout stores these in the same `food` collection as real food, + * and they have no `portion` and no `carbs`. `FoodMapper` already has an `else -> return null` + * branch for exactly this - it must be reached, not pre-empted by a parse failure. + */ + @Test + fun `a quickpick food document parses and is then dropped by the mapper`() { + val json = """{"type":"quickpick","name":"Breakfast","foods":[],"hidden":false,"position":0}""" + + val food = nsSdkJson.decodeFromString(RemoteFood.serializer(), json) + assertThat(food.type).isEqualTo("quickpick") + assertThat(food.portion).isEqualTo(0.0) + assertThat(food.carbs).isEqualTo(0) + + // and the mapper drops it, exactly as it did under Gson + assertThat(food.toNSFood()).isNull() + } + + /** + * The case that made this severe: one bad document must not take the good ones with it. This is + * what `v3/food` actually returns - a list, decoded in a single pass. + */ + @Test + fun `a quickpick in the middle of a list does not lose the real food`() { + val json = """ + [{"type":"quickpick","name":"Breakfast","foods":[],"hidden":false}, + {"type":"food","name":"Apple","portion":100.0,"carbs":12,"unit":"g"}, + {"type":"food","name":"Bread","portion":50.0,"carbs":25,"unit":"g"}] + """.trimIndent() + + val foods = nsSdkJson.decodeFromString(ListSerializer(RemoteFood.serializer()), json) + assertThat(foods).hasSize(3) + + val mapped = foods.mapNotNull { it.toNSFood() } + assertThat(mapped).hasSize(2) + assertThat(mapped.map { it.name }).containsExactly("Apple", "Bread") + } + + /** A third-party uploader writing an explicit null instead of omitting the field. */ + @Test + fun `a food document with explicit nulls still parses`() { + val json = """{"type":"food","name":"Apple","portion":100.0,"carbs":null,"unit":null}""" + + val food = nsSdkJson.decodeFromString(RemoteFood.serializer(), json) + assertThat(food.carbs).isEqualTo(0) + assertThat(food.name).isEqualTo("Apple") + } + + /** A history/deleted document, where the server strips the payload. */ + @Test + fun `a stripped food document parses instead of throwing`() { + val json = """{"identifier":"abc123","srvModified":1785992181588,"isValid":false}""" + + val food = nsSdkJson.decodeFromString(RemoteFood.serializer(), json) + assertThat(food.type).isEmpty() + assertThat(food.toNSFood()).isNull() // not type "food" -> dropped, as before + } + + /** The whole response shape the client actually decodes. */ + @Test + fun `a food response wrapper survives a mixed list`() { + val json = """{"result":[{"type":"quickpick","name":"QP"},{"type":"food","name":"Apple","portion":100.0,"carbs":12}]}""" + + val response = nsSdkJson.decodeFromString(NSResponse.serializer(ListSerializer(RemoteFood.serializer())), json) + assertThat(response.result).hasSize(2) + assertThat(response.result?.mapNotNull { it.toNSFood() }).hasSize(1) + } + + // ---------------------------------------------------------------- entries + + /** + * `entries` is the glucose feed. The endpoints the follower uses are **not** filtered by type, + * so a record without `type` reaches the decoder. Losing the page here would stop all glucose. + */ + @Test + fun `an entry without type parses and is skipped by both mappers`() { + val json = """{"date":1785992179555,"sgv":120.0,"device":"someUploader"}""" + + val entry = nsSdkJson.decodeFromString(RemoteEntry.serializer(), json) + assertThat(entry.type).isEmpty() + assertThat(entry.toCalibrationMbg()).isNull() + } + + @Test + fun `an entry with an explicit null type parses`() { + val json = """{"date":1785992179555,"sgv":120.0,"type":null}""" + + val entry = nsSdkJson.decodeFromString(RemoteEntry.serializer(), json) + assertThat(entry.type).isEmpty() + } + + /** One typeless record must not take the real glucose values with it. */ + @Test + fun `a typeless entry in a list does not lose the real readings`() { + val json = """ + [{"date":1785992179555,"sgv":120.0}, + {"date":1785992479555,"sgv":125.0,"type":"sgv","dateString":"2026-08-06T05:01:19.555Z"}, + {"date":1785992779555,"sgv":130.0,"type":"sgv","dateString":"2026-08-06T05:06:19.555Z"}] + """.trimIndent() + + val entries = nsSdkJson.decodeFromString(ListSerializer(RemoteEntry.serializer()), json) + assertThat(entries).hasSize(3) + assertThat(entries.count { it.type == "sgv" }).isEqualTo(2) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt new file mode 100644 index 000000000000..8e08541ec398 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt @@ -0,0 +1,129 @@ +package app.aaps.core.nssdk.mapper + +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Test + +/** + * A spike, not a contract test. + * + * `GsonTypeCoercionTest` pins what Gson does today with a wrong-typed field: it coerces a number + * into a `String`, and a numeric string into `Long` / `Double` / `Int` / `Boolean`. Before rewriting + * 190 annotations, this measures whether kotlinx.serialization can be **configured** to do the same, + * or whether every affected field needs a hand written serializer. Those are very different amounts + * of work, so the answer decides how the converter switch gets done. + * + * The mirror class below only carries the field shapes that matter, not the whole wire model. + */ +class KotlinxCoercionSpikeTest { + + @Serializable + private data class Mirror( + @SerialName("eventType") val eventType: String? = null, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("date") val date: Long? = null, + @SerialName("insulin") val insulin: Double? = null, + @SerialName("percentage") val percentage: Int? = null, + @SerialName("isValid") val isValid: Boolean? = null + ) + + /** Every case from GsonTypeCoercionTest that relies on coercion. */ + private val cases = listOf( + "number into String" to """{"created_at":1785992179555}""", + "numeric string into Long" to """{"date":"1785992179555"}""", + "numeric string into Double" to """{"insulin":"0.25"}""", + "numeric string into Int" to """{"percentage":"90"}""", + "quoted true into Boolean" to """{"isValid":"true"}""", + "integer into Double" to """{"insulin":4}""" + ) + + private fun outcomes(json: Json): Map = + cases.associate { (name, text) -> name to runCatching { json.decodeFromString(Mirror.serializer(), text) }.isSuccess } + + /** + * Strict is the default, and it is far closer to Gson than expected: a **quoted number** is + * already accepted by `Long`, `Double` and `Int` fields, and `"true"` by a `Boolean` field. + * + * Exactly one case differs - a bare number arriving in a `String` field - and that is the one + * `created_at` actually hits in the wild. So the gap between the two libraries here is a single + * behaviour, not the broad class of coercions it first looked like. + */ + @Test + fun `strict kotlinx differs from Gson in exactly one case`() { + val strict = outcomes(Json { ignoreUnknownKeys = true; explicitNulls = false }) + + assertThat(strict).isEqualTo( + mapOf( + "number into String" to false, // <- the only gap + "numeric string into Long" to true, + "numeric string into Double" to true, + "numeric string into Int" to true, + "quoted true into Boolean" to true, + "integer into Double" to true + ) + ) + } + + /** + * The question this spike exists to answer. If lenient covers everything, the converter switch + * is a configuration choice. If it does not, the uncovered cases each need a serializer. + */ + @Test + fun `lenient kotlinx - what it does and does not recover`() { + val lenient = outcomes(Json { ignoreUnknownKeys = true; explicitNulls = false; isLenient = true }) + + // Asserted as a whole map so a failure prints the full picture in one go. + assertThat(lenient).isEqualTo( + mapOf( + "number into String" to true, + "numeric string into Long" to true, + "numeric string into Double" to true, + "numeric string into Int" to true, + "quoted true into Boolean" to true, + "integer into Double" to true + ) + ) + } + + /** Values, not just "did it parse" - a coercion that silently produces the wrong number is worse. */ + @Test + fun `lenient kotlinx produces the same values as Gson`() { + val json = Json { ignoreUnknownKeys = true; explicitNulls = false; isLenient = true } + + assertThat(json.decodeFromString(Mirror.serializer(), """{"created_at":1785992179555}""").createdAt) + .isEqualTo("1785992179555") + assertThat(json.decodeFromString(Mirror.serializer(), """{"date":"1785992179555"}""").date) + .isEqualTo(1785992179555L) + assertThat(json.decodeFromString(Mirror.serializer(), """{"insulin":"0.25"}""").insulin) + .isEqualTo(0.25) + assertThat(json.decodeFromString(Mirror.serializer(), """{"percentage":"90"}""").percentage) + .isEqualTo(90) + assertThat(json.decodeFromString(Mirror.serializer(), """{"isValid":"true"}""").isValid) + .isTrue() + } + + /** Unknown keys and absent fields, the other two things the wire layer depends on. */ + @Test + fun `unknown keys are ignored and absent fields fall back to the default`() { + val json = Json { ignoreUnknownKeys = true; explicitNulls = false; isLenient = true } + val parsed = json.decodeFromString( + Mirror.serializer(), + """{"eventType":"Correction Bolus","somethingFromTheFuture":{"a":[1,2]}}""" + ) + + assertThat(parsed.eventType).isEqualTo("Correction Bolus") + assertThat(parsed.date).isNull() + assertThat(parsed.insulin).isNull() + } + + /** An explicit JSON null must land as Kotlin null, the way Gson leaves it. */ + @Test + fun `explicit nulls decode to null`() { + val json = Json { ignoreUnknownKeys = true; explicitNulls = false; isLenient = true } + val parsed = json.decodeFromString(Mirror.serializer(), """{"insulin":null,"carbs":null}""") + + assertThat(parsed.insulin).isNull() + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt index 8067c9c7564d..5465275d27a7 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt @@ -5,7 +5,7 @@ import app.aaps.core.nssdk.localmodel.treatment.NSCarbs import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryBasal import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryTarget import com.google.common.truth.Truth.assertThat -import com.google.gson.JsonSyntaxException +import kotlinx.serialization.SerializationException import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -157,29 +157,32 @@ class RealNightscoutTreatmentTest { } /** - * Malformed text **throws** today - `toNSTreatment()` is - * `Gson().fromJson(this, RemoteTreatment::class.java).toTreatment()` with no guard, even though - * its return type is nullable. This test records that, it does not endorse it. + * Malformed text still throws, and still is not caught anywhere - `toNSTreatment()` has no + * guard even though its return type is nullable. This test records that, it does not endorse it. * - * Pinned because the caller is unprotected: `NSClientV3Service.onDataCreateUpdate` is a - * socket.io listener with no try/catch, so whatever this throws escapes onto the socket - * callback thread. After the move to kotlinx.serialization the exception type would become - * `SerializationException`, and any `catch` further up would silently stop catching it. + * **The exception type changed** from Gson's `JsonSyntaxException` to + * `SerializationException`. That was called out before the migration precisely so it could not + * happen by accident: any `catch` further up written against the Gson type would silently stop + * catching. The callers were checked - `NSClientV3Service.onDataCreateUpdate` is a socket.io + * listener with no try/catch at all, so nothing was relying on the old type. * - * If the parser is ever made defensive, change this test on purpose - do not let a rewrite - * change it by accident. + * If the parser is ever made defensive, change this test on purpose. */ @Test - fun `malformed json throws - current behaviour, pinned`() { - assertThrows { "hello".toNSTreatment() } - assertThrows { """{"eventType":"Correction Bolus","date":""".toNSTreatment() } - assertThrows { """[{"eventType":"Correction Bolus"}]""".toNSTreatment() } - assertThrows { """{"eventType":"Correction Bolus","date":"not-a-number"}""".toNSTreatment() } + fun `malformed json throws - exception type changed on purpose`() { + assertThrows { "hello".toNSTreatment() } + assertThrows { """{"eventType":"Correction Bolus","date":""".toNSTreatment() } + assertThrows { """[{"eventType":"Correction Bolus"}]""".toNSTreatment() } + assertThrows { """{"eventType":"Correction Bolus","date":"not-a-number"}""".toNSTreatment() } } - /** Empty text is a separate case: Gson returns null, and the unguarded `.toTreatment()` then fails. */ + /** + * Empty text was a separate case under Gson: it returned null and the unguarded `.toTreatment()` + * then threw `NullPointerException`. kotlinx reports it as malformed input instead, which is the + * more honest answer for the same input. + */ @Test - fun `empty text throws NullPointerException - current behaviour, pinned`() { - assertThrows { "".toNSTreatment() } + fun `empty text throws - now reported as malformed rather than NPE`() { + assertThrows { "".toNSTreatment() } } } diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt new file mode 100644 index 000000000000..3f1e374bd1cf --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt @@ -0,0 +1,144 @@ +package app.aaps.core.nssdk.mapper + +import app.aaps.core.nssdk.localmodel.treatment.NSBolus +import app.aaps.core.nssdk.localmodel.treatment.NSCarbs +import com.google.common.truth.Truth.assertThat +import app.aaps.core.nssdk.nsSdkJson +import kotlinx.serialization.SerializationException +import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * Pins how the current parser reacts when a field arrives with the **wrong JSON type**. + * + * This is the part of the Gson to kotlinx.serialization move most likely to break quietly. Gson is + * permissive: it coerces a number into a `String` field, and a numeric string into a `Long` or + * `Double` field, without complaint. kotlinx.serialization is strict by default and throws instead. + * + * That matters because Nightscout data is not written by one uploader. `RemoteTreatment.created_at` + * is declared `String?` and its own comment in the model says: + * + * > "a lot of treatments don't have date, only created_at, some of them with string others with + * > long..." + * + * So mixed types in that field are not hypothetical, they are the documented reason the field looks + * the way it does. A strict parser would start throwing on records that parse fine today, and + * because `NSClientV3Service.onDataCreateUpdate` is a socket.io listener with no try/catch, that + * would escape onto the socket callback thread and drop the record. + * + * These tests record today's behaviour. After the converter switch they must still pass - if one + * cannot, that is a deliberate decision to make, not something to discover in the field. + */ +class WireTypeCoercionTest { + + private fun parse(json: String): RemoteTreatment = nsSdkJson.decodeFromString(RemoteTreatment.serializer(), json) + + // ---------------------------------------------------------------- number into a String field + + /** The documented real-world case: `created_at` sent as an epoch number, not a string. */ + @Test + fun `created_at as a number is coerced to a string`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":1785992179555,"insulin":0.25}""") + + assertThat(treatment.created_at).isEqualTo("1785992179555") + } + + /** + * And it still produces a usable timestamp, because `timestamp()` falls back to parsing + * `created_at` when `date` / `mills` are absent. + * + * Note what this shows: the epoch-number form does NOT round-trip into a timestamp. joda's ISO + * parser cannot read "1785992179555", so `fromISODateString` swallows the failure and returns 0. + * Pinned as-is - it is pre-existing, and fixing it is not part of a converter swap. + */ + @Test + fun `a numeric created_at does not survive into the timestamp`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":1785992179555}""") + + assertThat(treatment.timestamp()).isEqualTo(0L) + } + + /** The ISO string form, which does work. */ + @Test + fun `an ISO created_at parses into the timestamp`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":"2026-08-06T04:56:19.555Z"}""") + + assertThat(treatment.timestamp()).isEqualTo(1785992179555L) + } + + /** Unparseable text is swallowed rather than thrown - the record survives with timestamp 0. */ + @Test + fun `an unparseable created_at gives timestamp zero, it does not throw`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":"whenever"}""") + + assertThat(treatment.timestamp()).isEqualTo(0L) + } + + // ---------------------------------------------------------------- numeric string into a number field + + @Test + fun `a numeric string is coerced into a Long field`() { + val treatment = parse("""{"eventType":"Correction Bolus","date":"1785992179555"}""") + + assertThat(treatment.date).isEqualTo(1785992179555L) + } + + @Test + fun `a numeric string is coerced into a Double field`() { + val treatment = parse("""{"eventType":"Correction Bolus","insulin":"0.25"}""") + + assertThat(treatment.insulin).isEqualTo(0.25) + } + + @Test + fun `a numeric string is coerced into an Int field`() { + val treatment = parse("""{"eventType":"Profile Switch","percentage":"90"}""") + + assertThat(treatment.percentage).isEqualTo(90) + } + + /** A whole-number JSON value landing in a Double field. */ + @Test + fun `an integer is accepted by a Double field`() { + val treatment = parse("""{"eventType":"Carb Correction","carbs":4}""") + + assertThat(treatment.carbs).isEqualTo(4.0) + } + + // ---------------------------------------------------------------- booleans + + @Test + fun `the string true is coerced into a Boolean field`() { + val treatment = parse("""{"eventType":"Correction Bolus","isValid":"true"}""") + + assertThat(treatment.isValid).isTrue() + } + + // ---------------------------------------------------------------- what still throws + + /** Text that is not a number at all still fails, in both libraries. */ + @Test + fun `non numeric text in a number field throws`() { + assertThrows { parse("""{"eventType":"Correction Bolus","date":"not-a-number"}""") } + } + + // ---------------------------------------------------------------- end to end through the mapper + + /** + * The same coercions seen through the public entry point, so the pin covers what callers + * actually use rather than only the wire model. + */ + @Test + fun `coerced values survive through toNSTreatment`() { + val bolus = """{"eventType":"Correction Bolus","date":"1785992179555","insulin":"0.25","isValid":"true"}""" + .toNSTreatment() + assertThat(bolus).isInstanceOf(NSBolus::class.java) + assertThat((bolus as NSBolus).insulin).isEqualTo(0.25) + assertThat(bolus.date).isEqualTo(1785992179555L) + + val carbs = """{"eventType":"Carb Correction","date":1785987360000,"carbs":"4"}""".toNSTreatment() + assertThat(carbs).isInstanceOf(NSCarbs::class.java) + assertThat((carbs as NSCarbs).carbs).isEqualTo(4.0) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 841205660386..7358cd82ce5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -131,6 +131,7 @@ com-squareup-okhttp3-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", com-squareup-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } com-squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } +com-squareup-retrofit2-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } org-apache-commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = "3.20.0" } org-mozilla-rhino = { group = "org.mozilla", name = "rhino", version = "1.9.1" } From cb7b8fa924a6ecb0acea3981e3401b7753dd8893 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 15:18:01 +0200 Subject: [PATCH 007/146] :core:nssdk date parsing, eliminate joda --- _docs/KMP_IOS_FEASIBILITY.md | 181 +++++++++++++++--- core/nssdk/build.gradle.kts | 2 +- .../core/nssdk/remotemodel/RemoteTreatment.kt | 105 ++++++++-- .../core/nssdk/mapper/CreatedAtParsingTest.kt | 103 ++++++++++ .../core/nssdk/mapper/WireTypeCoercionTest.kt | 49 ++++- 5 files changed, 394 insertions(+), 46 deletions(-) create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index f521b7f58c76..9238af4254cf 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -57,10 +57,10 @@ So most of the Compose work of the last year can be reused. |--------------------------------------------------------|--------------------------------------------------------|----------------------------------------| | Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | | RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | -| Retrofit + OkHttp | 6 | Ktor client | +| Retrofit + OkHttp | 12 / 14, of which 4 / 3 in `:core:nssdk` | Ktor client | | socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | -| Gson | 34 | kotlinx.serialization | -| `org.json` (`JSONObject`) | 243 | kotlinx `JsonObject` - see section 8 | +| Gson | 46, **0 in `:core:nssdk`** | kotlinx.serialization - see section 8 | +| `org.json` (`JSONObject`) | 227, **0 in `:core:nssdk`** | kotlinx `JsonObject` - see section 8 | | WorkManager | 44 | See warning below | | joda-time | 5 | kotlinx-datetime | | `java.text.DecimalFormat` | 74 | Done, see section 8 | @@ -374,11 +374,22 @@ iOS, and makes every later step demand driven: a blocker is removed because it s and the next screen, not because it is on a list. **Where this stands.** Step 1 is done. Half of step 0 is done too, and out of order: `:core:data` -already builds for Kotlin/Native (wave 5), and part of step 5 has been pulled forward because -`:core:nssdk` turned out to be sliceable after all (wave 6). What is still missing from step 0 is the -half that needs a Mac - a real device, and an honest look at Compose Multiplatform on iOS. No amount -of further blocker removal answers that question, which is the argument for doing it soon rather than -continuing down the list. +already builds for Kotlin/Native (wave 5), and most of step 5 has been pulled forward because +`:core:nssdk` turned out to be sliceable after all - `org.json` in wave 6, Gson in wave 7. What is +left of that module is the HTTP client itself: + +| Left in `:core:nssdk` | Files | +| --- | --- | +| Retrofit / OkHttp | 4 / 3 | +| `java.io.IOException` (the exception hierarchy) | 1 | +| `android.*` (mainly `Context` for the OkHttp cache) | 2 | +| joda-time (a post-decode helper, not the wire contract) | 1 | + +All four go together with Ktor, which makes the rest of step 5 one piece of work rather than four. + +What is still missing from step 0 is the half that needs a Mac - a real device, and an honest look at +Compose Multiplatform on iOS. No amount of further blocker removal answers that question, which is +the argument for doing it soon rather than continuing down the list. --- @@ -402,6 +413,7 @@ worth keeping (see waves 5 and 6): | `67ecb1e696` | Going KMP - `:core:data` is a real multiplatform module | | `e24476237b` | Prepare `:core:nssdk` - dead code out, defaults in, characterization tests | | `f6a4e85e8a` | `:core:nssdk` `org.json` -> kotlinx | +| `350f486be4` | `:core:nssdk` Gson -> kotlinx.serialization | ### Wave 1 - `DecimalFormat` removed @@ -741,6 +753,112 @@ reads both go through the changed Gson type adapter, and kotlinx `JsonObject` al `Map`, so Gson picking its built-in map adapter instead of the registered one was a real possibility that would have failed quietly rather than thrown. +### Wave 7 - `:core:nssdk` off Gson (done) + +190 `@SerializedName` -> `@SerialName`, `@Serializable` on 19 classes, and the Retrofit converter +swapped for `converter-kotlinx-serialization`. That artifact is **official and ships with Retrofit +3.0.0**, same group and version as the Gson converter it replaces, so it needed no third party +dependency - and it goes away with Retrofit itself when the client moves to Ktor. + +Two things got smaller rather than bigger: + +- The Gson `JsonDeserializer` that built `JsonObject` is **gone**. kotlinx reads `JsonObject` + natively, so the adapter, the `GsonBuilder` and the `Gson` instance all went with it. +- `DeviceStatusMapper` used to rebuild all four schema-less subtrees (`pump.extended`, + `openaps.suggested` / `enacted` / `iob`) by printing each to text and parsing it back, because one + side was a Gson tree and the other a kotlinx tree. Both sides are kotlinx now, so those seven + conversions became a straight assignment. + +#### The configuration is the whole decision + +`NsSdkJson.kt` holds one `Json` instance shared by Retrofit and the string mappers. Every flag is +there to match Gson, not because it is a good default in the abstract: + +| Flag | Why | +| --- | --- | +| `ignoreUnknownKeys` | documents carry fields this version has never seen | +| `explicitNulls = false` | Gson omits nulls; NS rejects some explicit nulls | +| `isLenient` | one measured case: a bare **number** arriving in a `String` field (`created_at`) | +| `encodeDefaults` | **backward compatibility** - see below | +| `coerceInputValues` | **forward compatibility** - see below | + +Two of those five were added only because a test failed, and both are the difference between working +and quietly breaking somebody else's device: + +- **`encodeDefaults`** - kotlinx's default is the **opposite** of Gson's. Gson writes every non-null + field; kotlinx omits any field equal to its default. Without this, `isReadOnly = false`, + `duration = 0`, `utcOffset = 0` and every `LastModified.Collections` counter simply stop being + written. Absent is not the same as false to a reader that has not been updated. +- **`coerceInputValues`** - an **unknown enum value** throws in kotlinx but was mapped to `null` by + Gson. `eventType` is an enum. A *newer* AAPS adding one event type would otherwise make every older + client throw on that record, inside a socket.io listener with no try/catch. + +Only one case needed `isLenient`. Strict kotlinx already accepts a quoted number in a `Long` / +`Double` / `Int` field and `"true"` in a `Boolean` field, which was a pleasant surprise - the gap +between the two libraries is much narrower than it looks. + +#### The trap that nearly shipped + +**A non-null field with no default is optional under Gson and mandatory under kotlinx.** Gson builds +objects with `Unsafe.allocateInstance`, never calls the constructor, and leaves such a field as +`null` / `0` / `0.0`. kotlinx treats the identical declaration as required and throws. + +Neither `coerceInputValues` nor `explicitNulls` rescues it - **both only apply to properties that +already have a default.** And a page is decoded in one pass, so one bad document loses the whole +page, not one record. + +`RemoteFood` had four such fields. Nightscout's `food` collection also stores **quickpick** +documents written by the NS food editor, which have no `portion` and no `carbs` - `FoodMapper` +already has an `else -> return null` branch for exactly those, but the parse now died before the +mapper ran. Measured: a list of `[good food, quickpick]` returned **zero** items. `LoadFoodsWorker` +then fails, and it sits mid-chain, so it also cancels the profile, settings and devicestatus workers +every fifth loop. + +`RemoteEntry.type` was the same shape on the glucose feed, where `LoadBgWorker` only advances its +cursor after a successful decode - so one typeless record would have stopped all glucose and +re-requested the same page forever. + +Fixed by giving those fields the values Gson used to leave behind (`""`, `0`, `0.0`), which restores +the old tolerant behaviour exactly. `encodeDefaults = true` means the upload format does not change. + +**This was found by an adversarial audit, not by the tests.** The characterization tests were +written specifically to catch behaviour changes and they missed it, because there was **no food +decode test at all** - `FoodExtensionKtTest` is object-to-object and `LoadFoodsWorkerTest` mocks +`getFoods()`. A test suite only pins the paths somebody thought to write a test for. + +#### Verified on a device, both versions at once + +A Pixel emulator ran a **new master against a pre-KMP client** on a live Nightscout instance. Every +record type that can be created by hand was created and followed end to end: + +| Record | Old client result | +| --- | --- | +| Carbs | `◄ INSERT`, visible in Treatments history | +| Bolus (with nested `icfg`) | `◄ INSERT Bolus`, visible in Treatments history | +| Profile switch | `◄ INSERT ProfileSwitch` + `EffectiveProfileSwitch` | +| Temporary target (new **and** PATCH update) | `◄ INSERT TemporaryTarget` | +| Therapy event | `◄ INSERT TherapyEvent` | +| Glucose, devicestatus, settings | received and applied | + +The actual bytes are the best evidence. This is what the new master uploaded for a 20 g carb entry: + +```json +{"date":1786012366166,"utcOffset":0,"app":"AAPS","isValid":true, + "isReadOnly":false,"eventType":"Meal Bolus","carbs":20.0,"notes":""} +``` + +`utcOffset:0`, `isValid:true` and `isReadOnly:false` are all present - that is `encodeDefaults` +working. Without it those three vanish. Nulls are omitted, as Gson did. + +The strongest single result is the **signed client-control round trip**: the old client sent an +HMAC-signed envelope, the new master verified it and acked, and the old client verified the ack back. +A signature only verifies if the serialized bytes match, so that is close to proof rather than +inference. + +Not covered on the device: temp basal and extended bolus (need real pump or loop activity), and the +**food upload path does not exist** - `QueueCounter` has no food counter, AAPS syncs food +download-only. The food fix rests on unit tests. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old @@ -798,27 +916,36 @@ keeps the old contract on a public interface method. 6. **Still open: `wear/SmallestDoubleString.kt`** - the last `DecimalFormat` in a non-test file that is not the deliberate platform seam. It builds patterns from a runtime string, so it needs real logic rather than a mapping. -7. **Still open: the `:core:nssdk` converter switch.** Gson to kotlinx is atomic - 210 - `@SerializedName`, the joda date parsing and the `IOException` hierarchy move together. Two things - need deciding first: the `Json { }` configuration (`ignoreUnknownKeys = true`, `explicitNulls = - false`), and what to do about the ~8 **non-null** fields that were deliberately left without - defaults. Gson currently leaves those null through `Unsafe.allocateInstance`; kotlinx would throw. - For `RemoteStatusResponse` throwing is arguably correct, since `v3/status` always sends them. - `RemoteFood` from a foreign uploader is the real question. -8. **Still open: Retrofit converter, or straight to Ktor?** Adding - `retrofit2-kotlinx-serialization-converter` is the smaller step but adds a dependency that gets - thrown away when Ktor lands. Going straight to Ktor avoids that and is the actual destination, and - its failure mode ("no network") is louder than a serialization-only swap ("quietly wrong data"). - Leaning Ktor. +7. ~~The `:core:nssdk` converter switch.~~ **Done - see wave 7.** The joda parsing turned out **not** + to be part of the contract: it is a plain helper on `RemoteTreatment`, called after decoding, so it + moves on its own schedule. The ~8 non-null fields were the real answer to this question, and the + answer was "give them defaults" - see the trap in wave 7. `RemoteStatusResponse` was left strict on + purpose: `v3/status` is AAPS's own call to a server it just authenticated against, and a reply + missing those fields is not something to carry on from. +8. ~~Retrofit converter, or straight to Ktor?~~ **Decided: the converter first, and the reasoning in + the earlier version of this note was wrong.** It argued that Ktor's failure mode is louder, but + going straight to Ktor does not *replace* the serialization change, it *bundles* it - you get the + quiet-wrong-data risk anyway, plus transport risk, in one commit. Separating them meant the + characterization tests could actually do their job on the serialization half. The throwaway + dependency cost nothing in the end: Retrofit 3.0.0 ships an official + `converter-kotlinx-serialization`, so it was one catalog line and no third party code. 9. **Still open: the OkHttp disk cache needs a `Context`.** One of the two remaining `android.*` imports in `:core:nssdk`. Ktor on iOS needs either a different cache story or none. -10. **Still open: R8 has never run against the KMP module.** Both device checks were debug builds, so - minification against multiplatform metadata and the `expect` / `actual` pairs is untested. This is - the only real remaining risk on the branch. - -Waves 1 to 4 are committed on `dev`. Waves 5 and 6 are committed on `kmp/core-data-experiment`, both -verified on WSA against a live Nightscout. The `FoodManagement` comma defect in section 10 is found -but not fixed. +10. ~~R8 has never run.~~ **Not a risk: AAPS does not minify.** Both convention plugins + (`android-app-dependencies` and `android-module-dependencies`) set `isMinifyEnabled = false` for + the `release` build type, so R8 never shrinks or obfuscates and the usual + "kotlinx.serialization needs keep rules" problem cannot occur here. A release build was run + anyway: it succeeds, and all five generated `$$serializer` classes plus the Retrofit kotlinx + converter are present in the release dex. The `benchmark` variant (`initWith(release)` plus debug + signing) installs and starts clean; its runtime sync check is still outstanding because the + emulator lost DNS, which starved both apps equally. +11. **Still open: merge `kmp/core-data-experiment` into `dev`.** Waves 5 to 7 all live there. Nothing + argues against it any more - the branch was device-verified in mixed-version pairs - but it is a + real merge of a wire format change and deserves its own decision. + +Waves 1 to 4 are committed on `dev`. Waves 5 to 7 are committed on `kmp/core-data-experiment`, all +verified against a live Nightscout - waves 5 and 6 on WSA, wave 7 on an emulator running a new master +against a pre-KMP client. The `FoodManagement` comma defect in section 10 is found but not fixed. --- diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 4dcb51a41929..069ce9546dfc 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { implementation(libs.com.squareup.retrofit2.converter.kotlinx.serialization) api(libs.com.squareup.okhttp3.okhttp) api(libs.com.squareup.okhttp3.logging.interceptor) - api(libs.net.danlew.android.joda) + api(libs.kotlinx.datetime) api(libs.kotlin.stdlib.jdk8) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt index 4812c58dceed..3e7df06ee729 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt @@ -1,10 +1,14 @@ package app.aaps.core.nssdk.remotemodel import app.aaps.core.nssdk.localmodel.treatment.EventType +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toInstant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import org.joda.time.DateTime -import org.joda.time.format.ISODateTimeFormat /* * Depending on the type, different other fields are present. @@ -90,16 +94,95 @@ internal data class RemoteTreatment( @SerialName("isBasalInsulin") val isBasalInsulin: Boolean? = null // boolean "Bolus" ) { - fun timestamp(): Long { - return date ?: mills ?: timestamp ?: created_at?. let { fromISODateString(created_at) } ?: 0L + /** + * Best timestamp this record can offer, in milliseconds. + * + * `date` is what AAPS and every API v3 writer send, so it is almost always the answer. The rest + * of the chain is for older API v1 documents and other uploaders, which often carry only + * `created_at` - as the comment on that field says, "some of them with string others with long". + */ + fun timestamp(): Long = + date ?: mills ?: timestamp ?: created_at?.let { parseCreatedAt(it) } ?: 0L + + /** + * `created_at` arrives in two shapes and both have to work. + * + * It is declared `String?`, but a number in that position is coerced to its text form, so an + * epoch written as `1525383610088` reaches here as the **digits** `"1525383610088"`. joda's ISO + * parser cannot read that and used to throw, which the catch turned into `0L` - putting the + * treatment at the epoch, in 1970, with no error anywhere. The epoch form is tried first now. + * + * Seconds are not accepted on purpose. `1525383610` is a valid epoch in seconds and also a valid + * epoch in milliseconds (17 January 1970), and nothing in the document says which is meant, so + * guessing would trade a visible 1970 date for an invisible wrong one. + */ + private fun parseCreatedAt(createdAt: String): Long { + val trimmed = createdAt.trim() + // Digits only (optionally signed) means an epoch, not an ISO date. + if (trimmed.isNotEmpty() && trimmed.all { it.isDigit() || it == '-' }) + trimmed.toLongOrNull()?.let { return it } + return fromISODateString(trimmed) } - private fun fromISODateString(isoDateString: String): Long = - try { - val parser = ISODateTimeFormat.dateTimeParser() - val dateTime = DateTime.parse(isoDateString, parser) - dateTime.toDate().time - } catch (_: Exception) { - 0L + /** + * Lenient ISO 8601 parsing, matching what joda's `ISODateTimeFormat.dateTimeParser()` used to + * accept here. joda is a JVM library, so it cannot go to iOS; kotlinx-datetime is multiplatform + * but strict, and a strict parser would return `0L` for shapes that work today - putting the + * treatment in 1970 with no error anywhere. + * + * The trick is to take any explicit offset off the end **first**, then parse what is left as a + * plain local value. That way one small parser covers every shape rather than needing a format + * per variant: + * + * - `2026-08-06T04:56:19.555Z`, `...+02:00`, `...+0200`, `...-04:00` -> that exact instant + * - `2026-08-06T04:56:19.555`, `2026-08-06T04:56` -> **local** time, as joda read it + * - `2026-08-06` -> **local** midnight + * - lower case `t` / `z` -> accepted + * - anything else -> `0L`, never a throw + * + * `CreatedAtParsingTest` pins all of it against the values joda produced. + */ + private fun fromISODateString(isoDateString: String): Long { + val text = isoDateString.trim().uppercase() + if (text.isEmpty()) return 0L + + // Peel off a trailing zone designator, so the rest is a plain local date-time. + var local = text + var offset: UtcOffset? = null + if (text.endsWith("Z")) { + local = text.dropLast(1) + offset = UtcOffset.ZERO + } else { + OFFSET_AT_END.find(text)?.let { match -> + val parsed = runCatching { UtcOffset.parse(withOffsetColon(match.value)) }.getOrNull() + if (parsed != null) { + local = text.substring(0, match.range.first) + offset = parsed + } + } + } + + val zone = TimeZone.currentSystemDefault() + + runCatching { LocalDateTime.parse(local) }.getOrNull()?.let { dateTime -> + val instant = offset?.let { dateTime.toInstant(it) } ?: dateTime.toInstant(zone) + return instant.toEpochMilliseconds() + } + // Date only. joda gave local midnight, and a date without a time never carries an offset. + runCatching { LocalDate.parse(local) }.getOrNull()?.let { date -> + return date.atStartOfDayIn(zone).toEpochMilliseconds() } + return 0L + } + } + +// File scope, not a companion: `@Serializable` generates its own companion to carry `serializer()`, +// and declaring a private one here would make that private too. + +/** A trailing `+HH:MM` / `+HHMM` offset. Anchored so it cannot match the date's own dashes. */ +private val OFFSET_AT_END = Regex("""[+-]\d{2}:?\d{2}$""") + +/** `+0200` -> `+02:00`; already-correct input is returned unchanged. */ +private fun withOffsetColon(offset: String): String = + if (offset.contains(':')) offset else offset.substring(0, 3) + ":" + offset.substring(3) diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt new file mode 100644 index 000000000000..574db40b2486 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt @@ -0,0 +1,103 @@ +package app.aaps.core.nssdk.mapper + +import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test +import java.util.TimeZone + +/** + * Pins every `created_at` shape the parser has to cope with. + * + * This exists so joda can be removed from `:core:nssdk` without guessing. `ISODateTimeFormat + * .dateTimeParser()` is deliberately **lenient** - it accepts far more than RFC 3339 - while + * `kotlinx.datetime.Instant.parse` is strict. A straight swap would silently start returning `0L` + * for shapes that parse today, and `0L` means the treatment lands in 1970 with no error anywhere. + * + * `created_at` is only consulted when `date`, `mills` and `timestamp` are all absent, which AAPS + * never produces - but older API v1 documents and other uploaders do, and those are exactly the + * records with unusual date formats. + * + * Two of these shapes carry **no zone at all** and are therefore read as **local** time. Their + * expected value is computed from the machine's own offset rather than hard coded, so the test says + * what it means ("this is local time") and passes anywhere. Note that the JVM default zone cannot be + * swapped inside the test to check this: joda caches its own `DateTimeZone` default at class init + * and ignores a later `TimeZone.setDefault`. + */ +class CreatedAtParsingTest { + + private fun timestampOf(createdAt: String): Long = + nsSdkJson.decodeFromString( + RemoteTreatment.serializer(), + """{"eventType":"Correction Bolus","created_at":"$createdAt"}""" + ).timestamp() + + // 2026-08-06T04:56:19.555Z == 1785992179555 + private val utcMillis = 1785992179555L + private val midnightUtc = 1785974400000L + + /** Local offset at that moment, so the zone-less expectations hold in any time zone. */ + private fun offsetAt(instant: Long) = TimeZone.getDefault().getOffset(instant).toLong() + + @Test + fun `the shapes that must keep working`() { + val results = linkedMapOf( + "full UTC" to timestampOf("2026-08-06T04:56:19.555Z"), + "no millis" to timestampOf("2026-08-06T04:56:19Z"), + "offset with colon" to timestampOf("2026-08-06T06:56:19.555+02:00"), + "offset without colon" to timestampOf("2026-08-06T06:56:19.555+0200"), + "negative offset" to timestampOf("2026-08-06T00:56:19.555-04:00"), + "no zone at all" to timestampOf("2026-08-06T04:56:19.555"), + "no seconds" to timestampOf("2026-08-06T04:56Z"), + "date only" to timestampOf("2026-08-06"), + "one decimal" to timestampOf("2026-08-06T04:56:19.5Z"), + "lowercase t and z" to timestampOf("2026-08-06t04:56:19.555z") + ) + + // Asserted as one map so a single run shows the whole picture rather than the first failure. + assertThat(results).isEqualTo( + linkedMapOf( + "full UTC" to utcMillis, + "no millis" to 1785992179000L, + "offset with colon" to utcMillis, + "offset without colon" to utcMillis, + "negative offset" to utcMillis, + // no zone -> local wall clock, so the instant is earlier by the local offset + "no zone at all" to utcMillis - offsetAt(utcMillis), + "no seconds" to 1785992160000L, + "date only" to midnightUtc - offsetAt(midnightUtc), // local midnight + "one decimal" to 1785992179500L, + "lowercase t and z" to utcMillis + ) + ) + } + + /** + * The zone-less case stated on its own, because it is the one a stricter parser would get wrong + * in a way nothing else would notice: `Instant.parse` rejects it outright, and treating it as UTC + * would silently shift the treatment by the local offset. + */ + @Test + fun `a created_at without a zone is read as local time, not UTC`() { + val parsed = timestampOf("2026-08-06T04:56:19.555") + + assertThat(parsed).isEqualTo(utcMillis - offsetAt(utcMillis)) + // and it really is offset-dependent unless the machine happens to run on UTC + if (offsetAt(utcMillis) != 0L) assertThat(parsed).isNotEqualTo(utcMillis) + } + + /** Anything unparseable gives 0 rather than throwing - the record survives, dated 1970. */ + @Test + fun `unparseable text gives zero and does not throw`() { + assertThat(timestampOf("whenever")).isEqualTo(0L) + assertThat(timestampOf("")).isEqualTo(0L) + assertThat(timestampOf("not-a-date-at-all")).isEqualTo(0L) + } + + /** The epoch forms, which are handled before the ISO parser is reached. */ + @Test + fun `epoch forms are handled before ISO parsing`() { + assertThat(timestampOf("1785992179555")).isEqualTo(1785992179555L) + assertThat(timestampOf(" 1785992179555 ")).isEqualTo(1785992179555L) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt index 3f1e374bd1cf..8e63c4ec1f15 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt @@ -45,18 +45,53 @@ class WireTypeCoercionTest { } /** - * And it still produces a usable timestamp, because `timestamp()` falls back to parsing - * `created_at` when `date` / `mills` are absent. + * An epoch written as a number now survives into the timestamp. * - * Note what this shows: the epoch-number form does NOT round-trip into a timestamp. joda's ISO - * parser cannot read "1785992179555", so `fromISODateString` swallows the failure and returns 0. - * Pinned as-is - it is pre-existing, and fixing it is not part of a converter swap. + * This used to give `0L` - joda's ISO parser cannot read "1785992179555", and the catch turned + * that into the epoch, so the treatment landed in 1970 with no error anywhere. It was pinned as + * broken while the converter was being swapped, then fixed separately. + * + * Only reachable when `date`, `mills` and `timestamp` are all absent, which AAPS never produces + * but older API v1 documents and other uploaders do. */ @Test - fun `a numeric created_at does not survive into the timestamp`() { + fun `a numeric created_at now survives into the timestamp`() { val treatment = parse("""{"eventType":"Correction Bolus","created_at":1785992179555}""") - assertThat(treatment.timestamp()).isEqualTo(0L) + assertThat(treatment.timestamp()).isEqualTo(1785992179555L) + } + + /** The other three fields still win, in order - the fallback must not change that. */ + @Test + fun `date mills and timestamp still take precedence over created_at`() { + val allFour = """{"eventType":"Correction Bolus","date":111,"mills":222,"timestamp":333,"created_at":444}""" + assertThat(parse(allFour).timestamp()).isEqualTo(111L) + + val noDate = """{"eventType":"Correction Bolus","mills":222,"timestamp":333,"created_at":444}""" + assertThat(parse(noDate).timestamp()).isEqualTo(222L) + + val onlyTimestamp = """{"eventType":"Correction Bolus","timestamp":333,"created_at":444}""" + assertThat(parse(onlyTimestamp).timestamp()).isEqualTo(333L) + } + + /** A quoted epoch is the same value as a bare one - the coercion happens before we see it. */ + @Test + fun `a quoted epoch created_at also works`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":"1785992179555"}""") + + assertThat(treatment.timestamp()).isEqualTo(1785992179555L) + } + + /** + * Seconds are deliberately NOT converted. `1525383610` is a plausible epoch in seconds and also a + * real millisecond epoch (17 January 1970), and the document does not say which. Guessing would + * swap a visible 1970 date for an invisible wrong one. + */ + @Test + fun `an epoch in seconds is taken literally, not multiplied`() { + val treatment = parse("""{"eventType":"Correction Bolus","created_at":1525383610}""") + + assertThat(treatment.timestamp()).isEqualTo(1525383610L) } /** The ISO string form, which does work. */ From e92ec082d3e1f05d35426ac7d881f77daead7b1f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 16:32:21 +0200 Subject: [PATCH 008/146] :core:nssdk tests --- core/nssdk/build.gradle.kts | 5 + .../nssdk/networking/NetworkStackBuilder.kt | 21 +- .../networking/NightscoutRemoteService.kt | 14 + .../nssdk/networking/NsSdkAuthContractTest.kt | 252 +++++++++++++++++ .../networking/NsSdkErrorBodyContractTest.kt | 184 +++++++++++++ .../networking/NsSdkResponseContractTest.kt | 254 ++++++++++++++++++ .../networking/NsSdkStatusContractTest.kt | 178 ++++++++++++ .../nssdk/networking/NsSdkUrlContractTest.kt | 183 +++++++++++++ gradle/libs.versions.toml | 1 + 9 files changed, 1090 insertions(+), 2 deletions(-) create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 069ce9546dfc..6cdc0dc5e1a5 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -19,6 +19,11 @@ dependencies { api(libs.com.squareup.okhttp3.logging.interceptor) api(libs.kotlinx.datetime) + // Test only: a real HTTP server on localhost, so the same characterization tests run against + // Retrofit today and Ktor after the port. Ktor MockEngine could not do that - it only exists + // after the swap, so it could never pin the OLD behaviour. + testImplementation(libs.com.squareup.okhttp3.mockwebserver) + api(libs.kotlin.stdlib.jdk8) api(platform(libs.kotlinx.coroutines.bom)) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt index 31b7fc7d79af..9ea0628b5862 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt @@ -12,6 +12,23 @@ import java.util.concurrent.TimeUnit internal object NetworkStackBuilder { + /** + * Turns what the caller supplies into the Retrofit base URL. + * + * Production callers pass a bare host with an optional sub-path - `NSClientV3Plugin.setClient` + * strips the scheme first - so `host.com` and `host.com/ns` behave exactly as before. + * + * An input that already carries a scheme is used as it stands. That is what lets a unit test + * point the client at `http://localhost:`, and it also stops a stored `http://host` from + * turning into `https://http://host/api/`, which is what the old string concatenation produced + * (the caller only strips `https://`). + */ + internal fun toBaseUrl(hostOrUrl: String): String { + val trimmed = hostOrUrl.trimEnd('/') + val withScheme = if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) trimmed else "https://$trimmed" + return "$withScheme/api/" + } + @JvmSynthetic internal fun getApi( baseUrl: String, @@ -35,7 +52,7 @@ internal object NetworkStackBuilder { logger: HttpLoggingInterceptor.Logger ): Retrofit = Retrofit.Builder() - .baseUrl("https://$baseUrl/api/") + .baseUrl(toBaseUrl(baseUrl)) .client( getOkHttpClient( context = context, @@ -55,7 +72,7 @@ internal object NetworkStackBuilder { logger: HttpLoggingInterceptor.Logger ): Retrofit = Retrofit.Builder() - .baseUrl("https://$baseUrl/api/") + .baseUrl(toBaseUrl(baseUrl)) .client(getAuthRefreshOkHttpClient(context = context, logging = logging, logger = logger)) .addConverterFactory(converterFactory) .build() diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt index b3ba95b72c72..3a1522721dce 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt @@ -87,6 +87,20 @@ internal interface NightscoutRemoteService { @POST("v3/food") suspend fun createFood(@Body remoteFood: RemoteFood): Response + /** + * Update and delete for food are **left as they are on purpose. Do not "fix" the path.** + * + * The `{identifier}` placeholder is missing from the path, so Retrofit rejects these methods + * when it builds the request factory and **no request is ever sent** - the throw is swallowed by + * the broad catch in `NSClientV3Plugin`. Food edits therefore do not sync, and that is the + * current, accepted state: the matching endpoint is broken on the Nightscout side, so making + * AAPS send the request would not help. + * + * This matters for the move to Ktor. A hand-written URL would silently turn "never sends" + * into `PATCH /api/v3/food` and `DELETE /api/v3/food` with no identifier - a request against the + * whole collection on a live Nightscout. Keep these failing locally until Nightscout supports + * them, then change both sides together. + */ @PATCH("v3/food") suspend fun updateFood(@Body remoteFood: RemoteFood, @Path("identifier") identifier: String): Response diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt new file mode 100644 index 000000000000..75dd91f95a49 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt @@ -0,0 +1,252 @@ +package app.aaps.core.nssdk.networking + +import android.content.Context +import app.aaps.core.nssdk.NSAndroidClientImpl +import app.aaps.core.nssdk.exceptions.DateHeaderOutOfToleranceException +import app.aaps.core.nssdk.exceptions.InvalidAccessTokenException +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import mockwebserver3.Dispatcher +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.RecordedRequest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import java.nio.file.Files +import java.util.concurrent.atomic.AtomicInteger + +/** + * Pins the authentication behaviour of `NSAuthInterceptor`. + * + * This is the riskiest part of the move to Ktor, because Ktor's ready made `Auth` / `bearer` + * provider differs from this interceptor in four ways, and two of them lose data silently: + * + * 1. **The header is always sent, even when the token is empty.** `bearer` omits it instead. An + * unauthenticated request to Nightscout does not fail - it answers **200 with the anonymous + * role**, which flows into `lastStatus` -> `hasWritePermission` -> `DataSyncWorker` skipping the + * entire upload block. Nothing is logged and nothing throws; uploads simply stop. + * 2. **Refresh fires on 401 and 403.** `bearer` only handles 401. + * 3. The failing body is inspected **before** refreshing, to detect a clock-skew rejection. A Ktor + * refresh hook never sees the body, so that error would disappear. + * 4. `bearer` serialises refreshes behind a `Mutex`; this code does not. That one is an improvement + * and may be adopted, but on purpose and with its own test - not as a side effect. + * + * Everything here is asserted through a real localhost server, so the file runs unchanged before and + * after the port. + */ +class NsSdkAuthContractTest { + + private lateinit var server: MockWebServer + private lateinit var client: NSAndroidClientImpl + private lateinit var cacheDir: File + + private val refreshCalls = AtomicInteger(0) + private var lastRefreshTarget: String? = null + private var refreshHadAuthHeader: Boolean? = null + + @BeforeEach + fun setUp() { + cacheDir = Files.createTempDirectory("nssdk-auth-cache").toFile() + server = MockWebServer() + server.start() + val context = mock { on { getCacheDir() } doReturn cacheDir } + client = NSAndroidClientImpl( + baseUrl = server.url("/").toString().trimEnd('/'), + accessToken = "REFRESH_TOKEN", + context = context, + logging = false, + logger = { }, + dispatcher = Dispatchers.Unconfined + ) + } + + @AfterEach + fun tearDown() { + server.close() + cacheDir.deleteRecursively() + } + + /** + * Routes by path: the token refresh endpoint is answered with [refresh], everything else with + * [data]. Using a dispatcher rather than a queue keeps the multi request flows readable. + */ + private fun route(data: () -> MockResponse, refresh: () -> MockResponse) { + server.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val target = request.target + return if (target.contains("/authorization/request/")) { + refreshCalls.incrementAndGet() + lastRefreshTarget = target + refreshHadAuthHeader = request.headers["Authorization"] != null + refresh() + } else data() + } + } + } + + private fun ok(body: String = """{"result":[]}""") = MockResponse.Builder().code(200).body(body).build() + private fun status(code: Int, body: String = "") = MockResponse.Builder().code(code).body(body).build() + + // ---------------------------------------------------------------- headers on every request + + /** + * The empty-token case, stated on its own because it is the silent one. On the very first call + * `jwtToken` is still `""`, so the code builds `"Bearer $jwtToken"` = `"Bearer "` - and the HTTP + * layer strips the trailing space, so what actually goes on the wire is the bare word `Bearer`. + * + * The exact text matters: the point is that the header **is present**. Omitting it makes + * Nightscout answer 200 with the anonymous role instead of failing. + */ + @Test + fun `Authorization is sent even before any token exists`() = runTest { + server.enqueue(ok()) + + client.getSgvs() + + val sent = server.takeRequest() + assertThat(sent.headers["Authorization"]).isEqualTo("Bearer") + } + + /** `Date` is bare epoch milliseconds, not an RFC 1123 date. Nightscout compares it to its own clock. */ + @Test + fun `Date is sent as bare epoch milliseconds`() = runTest { + server.enqueue(ok()) + + client.getSgvs() + + val date = server.takeRequest().headers["Date"] + assertThat(date).isNotNull() + assertThat(date!!.all { it.isDigit() }).isTrue() + // sane range rather than an exact value: milliseconds, not seconds + assertThat(date.toLong()).isGreaterThan(1_600_000_000_000L) + } + + // ---------------------------------------------------------------- refresh triggers + + @Test + fun `a 401 triggers exactly one refresh and one replay`() = runTest { + val dataCalls = AtomicInteger(0) + route( + data = { if (dataCalls.incrementAndGet() == 1) status(401, "unauthorized") else ok() }, + refresh = { ok("""{"token":"NEW_JWT","iat":1,"exp":2}""") } + ) + + val response = client.getSgvs() + + assertThat(response.values).isEmpty() + assertThat(refreshCalls.get()).isEqualTo(1) + assertThat(dataCalls.get()).isEqualTo(2) // original + one replay, never more + } + + /** 403 must refresh too - Ktor's bearer provider would ignore it. */ + @Test + fun `a 403 also triggers a refresh`() = runTest { + val dataCalls = AtomicInteger(0) + route( + data = { if (dataCalls.incrementAndGet() == 1) status(403, "forbidden") else ok() }, + refresh = { ok("""{"token":"NEW_JWT","iat":1,"exp":2}""") } + ) + + client.getSgvs() + + assertThat(refreshCalls.get()).isEqualTo(1) + assertThat(dataCalls.get()).isEqualTo(2) + } + + /** After a successful refresh the replay carries the NEW token. */ + @Test + fun `the replay carries the refreshed token`() = runTest { + val seenAuth = mutableListOf() + val dataCalls = AtomicInteger(0) + server.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse = + if (request.target.contains("/authorization/request/")) ok("""{"token":"NEW_JWT","iat":1,"exp":2}""") + else { + seenAuth += request.headers["Authorization"] + if (dataCalls.incrementAndGet() == 1) status(401, "unauthorized") else ok() + } + } + + client.getSgvs() + + assertThat(seenAuth).hasSize(2) + assertThat(seenAuth[0]).isEqualTo("Bearer") // no token yet, trailing space stripped + assertThat(seenAuth[1]).isEqualTo("Bearer NEW_JWT") // refreshed + } + + // ---------------------------------------------------------------- the refresh call itself + + /** The refresh must not carry an Authorization header, or it would recurse into itself. */ + @Test + fun `the refresh request carries no Authorization header and uses the refresh token in the path`() = runTest { + val dataCalls = AtomicInteger(0) + route( + data = { if (dataCalls.incrementAndGet() == 1) status(401, "unauthorized") else ok() }, + refresh = { ok("""{"token":"NEW_JWT","iat":1,"exp":2}""") } + ) + + client.getSgvs() + + assertThat(refreshHadAuthHeader).isFalse() + assertThat(lastRefreshTarget).isEqualTo("/api/v2/authorization/request/REFRESH_TOKEN") + } + + // ---------------------------------------------------------------- refresh outcomes + + /** A refresh that is itself rejected means the refresh token is bad - a distinct, non-retried error. */ + @Test + fun `a rejected refresh throws InvalidAccessToken and is not retried`() = runTest { + route(data = { status(401, "unauthorized") }, refresh = { status(401, "nope") }) + + assertThrows { client.getSgvs() } + assertThat(refreshCalls.get()).isEqualTo(1) // excluded from retry, so exactly one attempt + } + + /** + * A refresh that fails for another reason (server error) returns the ORIGINAL 401, which the read + * path then maps to a 4xx error. That one is not excluded from retry, so the whole flow repeats. + */ + @Test + fun `a refresh that errors falls back to the original response`() = runTest { + route(data = { status(401, "unauthorized") }, refresh = { status(500, "boom") }) + + assertThrows { client.getSgvs() } + // 4 attempts of (data + refresh), because InvalidParameterNightscoutException is retried + assertThat(refreshCalls.get()).isEqualTo(4) + } + + // ---------------------------------------------------------------- clock skew + + /** + * The clock-skew path. Nightscout rejects a request whose `Date` is too far from its own clock, + * and refreshing would not help, so the body is checked BEFORE any refresh is attempted. + * + * A Ktor refresh hook never sees the response body, which is why this cannot be expressed with + * the stock `bearer` provider. + */ + @Test + fun `a clock skew rejection throws before any refresh is attempted`() = runTest { + route(data = { status(401, "Date header out of tolerance") }, refresh = { ok() }) + + assertThrows { client.getSgvs() } + assertThat(refreshCalls.get()).isEqualTo(0) // never even tried + } + + /** The check is a plain substring match on the body, so surrounding JSON does not hide it. */ + @Test + fun `the clock skew message is matched inside a JSON body`() = runTest { + route( + data = { status(401, """{"status":401,"message":"Date header out of tolerance, check your clock"}""") }, + refresh = { ok() } + ) + + assertThrows { client.getSgvs() } + assertThat(refreshCalls.get()).isEqualTo(0) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt new file mode 100644 index 000000000000..3d63e2ad02a4 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt @@ -0,0 +1,184 @@ +package app.aaps.core.nssdk.networking + +import android.content.Context +import app.aaps.core.nssdk.NSAndroidClientImpl +import app.aaps.core.nssdk.localmodel.entry.Direction +import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 +import app.aaps.core.nssdk.localmodel.entry.NsUnits +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import java.nio.file.Files + +/** + * Pins the behaviour that depends on reading the **raw text of an error body**. + * + * Nightscout refuses a record whose `utcOffset` it does not like, with + * `400 Bad or missing utcOffset field`. The client recognises that by a plain `contains` on the + * body text and re-sends the record with `utcOffset = 0` + * (`NSAndroidClientImpl.createSgv` / `createCalibration` / `createTreatment`). + * + * If a rewrite loses that text - by reading the body only on success, by consuming the stream before + * it is inspected, or by decoding it into a typed model - the retry never happens. The 400 then + * falls through to a plain `CreateUpdateResponse(400, ...)`, `NSClientV3Plugin` logs one `FAIL` line + * and **advances the sync cursor anyway**, so that reading or treatment is never uploaded again. + * No exception, no repeat attempt, one line in a log nobody reads. + * + * Ktor has no separate `errorBody()`. The body must be read once, up front, and reused - which is + * exactly the trap this test exists to catch. + */ +class NsSdkErrorBodyContractTest { + + private lateinit var server: MockWebServer + private lateinit var client: NSAndroidClientImpl + private lateinit var cacheDir: File + + @BeforeEach + fun setUp() { + cacheDir = Files.createTempDirectory("nssdk-errorbody-cache").toFile() + server = MockWebServer() + server.start() + val context = mock { on { getCacheDir() } doReturn cacheDir } + client = NSAndroidClientImpl( + baseUrl = server.url("/").toString().trimEnd('/'), + accessToken = "token", + context = context, + logging = false, + logger = { }, + dispatcher = Dispatchers.Unconfined + ) + } + + @AfterEach + fun tearDown() { + server.close() + cacheDir.deleteRecursively() + } + + private fun sgv(utcOffset: Long) = NSSgvV3( + date = 1785992179555, + device = "test", + identifier = null, + utcOffset = utcOffset, + isValid = true, + sgv = 120.0, + units = NsUnits.MG_DL, + direction = Direction.FLAT, + noise = null, + filtered = null, + unfiltered = null + ) + + // ---------------------------------------------------------------- the utcOffset fallback + + /** + * The whole point. A 400 naming `utcOffset` must cause a second POST carrying `utcOffset: 0`, + * and the caller must see the result of that second attempt, not the first failure. + */ + @Test + fun `a utcOffset rejection is retried once with utcOffset zero`() = runTest { + server.enqueue( + MockResponse.Builder().code(400) + .body("""{"status":400,"message":"Bad or missing utcOffset field"}""").build() + ) + server.enqueue(MockResponse.Builder().code(201).body("""{"identifier":"abc123"}""").build()) + + val response = client.createSgv(sgv(utcOffset = 120)) + + assertThat(response.response).isEqualTo(201) + assertThat(response.identifier).isEqualTo("abc123") + assertThat(server.requestCount).isEqualTo(2) + + // the first attempt carried the real offset, the second carried zero + val first = Json.parseToJsonElement(server.takeRequest().body!!.utf8()).jsonObject + val second = Json.parseToJsonElement(server.takeRequest().body!!.utf8()).jsonObject + assertThat(first["utcOffset"]?.jsonPrimitive?.content).isEqualTo("120") + assertThat(second["utcOffset"]?.jsonPrimitive?.content).isEqualTo("0") + } + + /** + * The fallback must not loop. A record already at `utcOffset = 0` is not re-sent, because the + * guard is `nsSgvV3.utcOffset != 0L`. + */ + @Test + fun `a record already at zero is not retried`() = runTest { + repeat(4) { + server.enqueue( + MockResponse.Builder().code(400) + .body("""{"status":400,"message":"Bad or missing utcOffset field"}""").build() + ) + } + + val response = client.createSgv(sgv(utcOffset = 0)) + + assertThat(response.response).isEqualTo(400) + assertThat(server.requestCount).isEqualTo(1) + } + + /** A different 400 must NOT trigger the fallback - only the utcOffset wording does. */ + @Test + fun `an unrelated 400 is returned as is`() = runTest { + server.enqueue( + MockResponse.Builder().code(400).body("""{"status":400,"message":"something else"}""").build() + ) + + val response = client.createSgv(sgv(utcOffset = 120)) + + assertThat(response.response).isEqualTo(400) + assertThat(response.errorResponse).contains("something else") + assertThat(server.requestCount).isEqualTo(1) + } + + // ---------------------------------------------------------------- the other body match + + /** + * `cannot be modified by the client` means the record exists with a field AAPS cannot change. + * It is reported back with the body text and must not be retried. + */ + @Test + fun `a cannot-be-modified rejection is returned with its body and not retried`() = runTest { + val body = """{"status":400,"message":"field cannot be modified by the client"}""" + server.enqueue(MockResponse.Builder().code(400).body(body).build()) + + val response = client.createSgv(sgv(utcOffset = 120)) + + assertThat(response.response).isEqualTo(400) + assertThat(response.errorResponse).isEqualTo(body) + assertThat(server.requestCount).isEqualTo(1) + } + + // ---------------------------------------------------------------- the body must survive at all + + /** The verbatim server body reaches the caller - not a summary, not null. */ + @Test + fun `the error body is passed through verbatim`() = runTest { + val body = """{"status":422,"message":"unprocessable","detail":["a","b"]}""" + server.enqueue(MockResponse.Builder().code(422).body(body).build()) + + val response = client.createSgv(sgv(utcOffset = 120)) + + assertThat(response.errorResponse).isEqualTo(body) + } + + /** `app` is stamped onto every uploaded entry - Nightscout uses it to attribute the record. */ + @Test + fun `uploaded entries are stamped with the app name`() = runTest { + server.enqueue(MockResponse.Builder().code(201).body("""{"identifier":"abc"}""").build()) + + client.createSgv(sgv(utcOffset = 120)) + + val sent = Json.parseToJsonElement(server.takeRequest().body!!.utf8()).jsonObject + assertThat(sent["app"]?.jsonPrimitive?.content).isEqualTo("AAPS") + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt new file mode 100644 index 000000000000..59730af13318 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt @@ -0,0 +1,254 @@ +package app.aaps.core.nssdk.networking + +import android.content.Context +import app.aaps.core.nssdk.NSAndroidClientImpl +import app.aaps.core.nssdk.exceptions.InvalidFormatNightscoutException +import app.aaps.core.nssdk.localmodel.entry.Direction +import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 +import app.aaps.core.nssdk.localmodel.entry.NsUnits +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import java.nio.file.Files + +/** + * Pins how a response is turned into values: the **ETag**, which drives the sync cursor, and the + * **`{"result": ...}` envelope**, which is not applied consistently across endpoints. + * + * The envelope is the nastier of the two. Some endpoints answer with the document directly and some + * wrap it, and there is no rule - it has to be copied per endpoint. Because `nsSdkJson` sets + * `ignoreUnknownKeys = true`, guessing wrong does **not** fail: the wrapper is treated as an unknown + * key, every field comes back null, and the caller sees a successful response full of nothing. For a + * create that means the returned `identifier` is null, and `NSClientV3Plugin` then re-uploads that + * record as a brand new document on every sync, forever. + * + * The ETag matters because `lastServerModified` is what advances the paging cursor. A null there + * makes the worker skip the cursor update, so the same window is fetched again next time. + */ +class NsSdkResponseContractTest { + + private lateinit var server: MockWebServer + private lateinit var client: NSAndroidClientImpl + private lateinit var cacheDir: File + + @BeforeEach + fun setUp() { + cacheDir = Files.createTempDirectory("nssdk-response-cache").toFile() + server = MockWebServer() + server.start() + val context = mock { on { getCacheDir() } doReturn cacheDir } + client = NSAndroidClientImpl( + baseUrl = server.url("/").toString().trimEnd('/'), + accessToken = "token", + context = context, + logging = false, + logger = { }, + dispatcher = Dispatchers.Unconfined + ) + } + + @AfterEach + fun tearDown() { + server.close() + cacheDir.deleteRecursively() + } + + private fun sgv() = NSSgvV3( + date = 1785992179555, device = "test", identifier = "id-1", utcOffset = 0, + isValid = true, sgv = 120.0, units = NsUnits.MG_DL, direction = Direction.FLAT, + noise = null, filtered = null, unfiltered = null + ) + + // ================================================================ ETag + + /** + * Nightscout sends a weak ETag, `W/""`. The client strips the first three characters and + * the closing quote with `substring(3, length - 1)` and parses what is left as a Long. + */ + @Test + fun `a weak ETag becomes lastServerModified`() = runTest { + server.enqueue( + MockResponse.Builder().code(200) + .addHeader("ETag", """W/"1785992179555"""") + .body("""{"result":[]}""").build() + ) + + val response = client.getSgvsModifiedSince(1700000000000, 100) + + assertThat(response.lastServerModified).isEqualTo(1785992179555L) + } + + /** No ETag means no cursor update. The worker leaves `lastLoadedSrvModified` where it was. */ + @Test + fun `an absent ETag gives a null lastServerModified`() = runTest { + server.enqueue(MockResponse.Builder().code(200).body("""{"result":[]}""").build()) + + val response = client.getSgvsModifiedSince(1700000000000, 100) + + assertThat(response.lastServerModified).isNull() + } + + /** + * The parse is positional, not a pattern match, so a header that is not `W/"..."` breaks it. + * Recorded as it is: the exception escapes, and because it is not one of the three excluded + * types it is retried first - four requests, then it surfaces. + * + * Real Nightscout always sends the weak form, so this is a latent edge rather than a live bug. + * It is pinned so the port neither hides it nor makes it worse. + */ + @Test + fun `a malformed ETag throws after the retries - current behaviour, pinned`() = runTest { + repeat(8) { + server.enqueue( + MockResponse.Builder().code(200) + .addHeader("ETag", "not-an-etag") + .body("""{"result":[]}""").build() + ) + } + + assertThrows { client.getSgvsModifiedSince(1700000000000, 100) } + assertThat(server.requestCount).isEqualTo(4) + } + + // ================================================================ envelope + + /** + * `createEntry` answers with the create/update document **directly**, no `result` wrapper. + * `identifier` must survive - without it the reading is uploaded again as a new document forever. + */ + @Test + fun `entries create reads a bare body`() = runTest { + server.enqueue( + MockResponse.Builder().code(201) + .body("""{"identifier":"abc123","isDeduplication":false,"lastModified":1785992179555}""").build() + ) + + val response = client.createSgv(sgv()) + + assertThat(response.identifier).isEqualTo("abc123") + assertThat(response.lastModified).isEqualTo(1785992179555L) + } + + /** + * The failure mode, stated explicitly. Feeding the WRAPPED shape to that same bare endpoint does + * not throw - `ignoreUnknownKeys` swallows `result` and every field comes back null. + * + * This is what a wrong envelope choice looks like during the port: a green build, a 201, and a + * record that is re-uploaded on every sync. + */ + @Test + fun `a wrapped body on a bare endpoint yields nulls instead of failing`() = runTest { + server.enqueue( + MockResponse.Builder().code(201) + .body("""{"result":{"identifier":"abc123","lastModified":1785992179555}}""").build() + ) + + val response = client.createSgv(sgv()) + + assertThat(response.response).isEqualTo(201) // looks fine + assertThat(response.identifier).isNull() // but the identifier is gone + } + + /** + * `updateSvg` **ignores the response body completely** - it always returns `identifier = null` + * and reports only the status. So the envelope shape does not matter for this endpoint, and a + * port must not "helpfully" start reading the identifier here: callers rely on getting null. + */ + @Test + fun `entries update reports only the status and never an identifier`() = runTest { + server.enqueue( + MockResponse.Builder().code(200) + .body("""{"result":{"identifier":"abc123","lastModified":1785992179555}}""").build() + ) + + val response = client.updateSvg(sgv()) + + assertThat(response.response).isEqualTo(200) + assertThat(response.identifier).isNull() + assertThat(response.lastModified).isNull() + } + + /** 404 on an update is a success too - the record is simply not there any more. */ + @Test + fun `entries update treats 404 as success`() = runTest { + server.enqueue(MockResponse.Builder().code(404).body("""{"status":404}""").build()) + + val response = client.updateSvg(sgv()) + + assertThat(response.response).isEqualTo(404) + assertThat(server.requestCount).isEqualTo(1) + } + + /** + * A record with no identifier cannot be updated, and that is refused **before any request is + * made**. `InvalidFormatNightscoutException` is one of the three types excluded from retry, so + * it surfaces immediately rather than after four attempts. + */ + @Test + fun `updating a record without an identifier throws before any request`() = runTest { + val noIdentifier = NSSgvV3( + date = 1785992179555, device = "test", identifier = null, utcOffset = 0, + isValid = true, sgv = 120.0, units = NsUnits.MG_DL, direction = Direction.FLAT, + noise = null, filtered = null, unfiltered = null + ) + + assertThrows { client.updateSvg(noIdentifier) } + assertThat(server.requestCount).isEqualTo(0) + } + + /** Settings create is bare, like entries create. */ + @Test + fun `settings create reads a bare body`() = runTest { + server.enqueue(MockResponse.Builder().code(201).body("""{"identifier":"aaps"}""").build()) + + val response = client.createSettings(JsonObject(mapOf("a" to JsonPrimitive(1)))) + + assertThat(response.identifier).isEqualTo("aaps") + } + + /** Settings read is wrapped, and the document inside is schema-less. */ + @Test + fun `settings read unwraps the result`() = runTest { + server.enqueue( + MockResponse.Builder().code(200) + .body("""{"result":{"identifier":"aaps","runningConfig":{"pump":"Dana"}}}""").build() + ) + + val response = client.getSettings("aaps") + + assertThat(response.values).isNotNull() + assertThat(response.values!!.keys).containsAtLeast("identifier", "runningConfig") + } + + /** List reads are wrapped, and an empty list must stay an empty list rather than becoming null. */ + @Test + fun `an empty wrapped list stays empty`() = runTest { + server.enqueue(MockResponse.Builder().code(200).body("""{"result":[]}""").build()) + + val response = client.searchSettings(100) + + assertThat(response.values).isNotNull() + assertThat(response.values).isEmpty() + } + + /** A missing `result` on a wrapped endpoint gives an empty list, not a crash. */ + @Test + fun `a wrapped endpoint with no result gives an empty list`() = runTest { + server.enqueue(MockResponse.Builder().code(200).body("""{}""").build()) + + val response = client.searchSettings(100) + + assertThat(response.values).isEmpty() + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt new file mode 100644 index 000000000000..0e775b9a3d1f --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt @@ -0,0 +1,178 @@ +package app.aaps.core.nssdk.networking + +import android.content.Context +import app.aaps.core.nssdk.NSAndroidClientImpl +import app.aaps.core.nssdk.exceptions.InvalidParameterNightscoutException +import app.aaps.core.nssdk.exceptions.UnsuccessfulNightscoutException +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import java.nio.file.Files + +/** + * Pins how each HTTP status is turned into a result, and **how many requests that costs**. + * + * The request count is the important half. `callWrapper` wraps every call in `retry`, which makes + * **4 attempts** unless the exception's exact class is one of three excluded types + * (`NSAndroidClientImpl.callWrapper`, `utils/CoroutineUtils.kt`). So the number of requests is a + * direct, observable readout of whether an error path throws or returns: + * + * | path | 4xx behaviour | requests | + * | --- | --- | --- | + * | read (`getSgvs`, `searchSettings`) | throws `InvalidParameterNightscoutException` | **4** | + * | write (`createSettings`) | returns `CreateUpdateResponse(response = 4xx)` | **1** | + * | `getSettings` 404 | returns `ReadResponse(404, values = null)` | **1** | + * | `deleteSettings` 404 | treated as an already-done delete | **1** | + * + * This asymmetry is what Ktor's `expectSuccess = true` would destroy: every non-2xx would throw, + * the write ladders would become dead code, and because `ClientRequestException` is not in the + * exclusion list it would be retried 4 times before surfacing. `NSClientV3Plugin` would then stop + * advancing the sync cursor for that collection - quietly, with one log line. + * + * Driven through a real localhost server, so this file runs unchanged before and after the port. + */ +class NsSdkStatusContractTest { + + private lateinit var server: MockWebServer + private lateinit var client: NSAndroidClientImpl + private lateinit var cacheDir: File + + @BeforeEach + fun setUp() { + cacheDir = Files.createTempDirectory("nssdk-status-cache").toFile() + server = MockWebServer() + server.start() + val context = mock { on { getCacheDir() } doReturn cacheDir } + client = NSAndroidClientImpl( + baseUrl = server.url("/").toString().trimEnd('/'), + accessToken = "token", + context = context, + logging = false, + logger = { }, + dispatcher = Dispatchers.Unconfined + ) + } + + @AfterEach + fun tearDown() { + server.close() + cacheDir.deleteRecursively() + } + + /** Answer every request with the same status and body - enough for the retry cases. */ + private fun alwaysRespond(code: Int, body: String = "") { + repeat(8) { server.enqueue(MockResponse.Builder().code(code).body(body).build()) } + } + + // ---------------------------------------------------------------- read paths + + @Test + fun `a 4xx on a read throws InvalidParameter and is retried four times`() = runTest { + alwaysRespond(400, """{"status":400,"message":"bad request"}""") + + assertThrows { client.getSgvs() } + assertThat(server.requestCount).isEqualTo(4) + } + + @Test + fun `a 5xx on a read throws Unsuccessful and is retried four times`() = runTest { + alwaysRespond(500, "server exploded") + + assertThrows { client.getSgvs() } + assertThat(server.requestCount).isEqualTo(4) + } + + /** A successful call must cost exactly one request - the baseline the counts above are read against. */ + @Test + fun `a successful read makes exactly one request`() = runTest { + server.enqueue(MockResponse.Builder().code(200).body("""{"result":[]}""").build()) + + val response = client.getSgvs() + + assertThat(response.values).isEmpty() + assertThat(server.requestCount).isEqualTo(1) + } + + // ---------------------------------------------------------------- 404 as a normal answer + + /** `getSettings` turns 404 into an empty result, because "no settings doc yet" is not an error. */ + @Test + fun `getSettings maps 404 to a null value without throwing`() = runTest { + alwaysRespond(404, """{"status":404}""") + + val response = client.getSettings("aaps") + + assertThat(response.code).isEqualTo(404) + assertThat(response.values).isNull() + assertThat(server.requestCount).isEqualTo(1) + } + + /** + * A delete of something already gone is a success. `NSClientV3Plugin` relies on this to stop + * retrying a tombstoned identifier forever. + */ + @Test + fun `deleteSettings treats 404 as done`() = runTest { + alwaysRespond(404, """{"status":404}""") + + val response = client.deleteSettings("aaps_x") + + assertThat(response.response).isEqualTo(404) + assertThat(server.requestCount).isEqualTo(1) + } + + // ---------------------------------------------------------------- write paths + + /** + * A write never throws on 4xx. It returns the status **and the verbatim server body**, which the + * caller inspects - see `NsSdkErrorBodyContractTest` for the `utcOffset` path that depends on it. + */ + @Test + fun `a 4xx on a write returns the status and body, and makes one request`() = runTest { + val body = """{"status":400,"message":"Bad or missing utcOffset field"}""" + alwaysRespond(400, body) + + val response = client.createSettings(JsonObject(mapOf("a" to JsonPrimitive(1)))) + + assertThat(response.response).isEqualTo(400) + assertThat(response.errorResponse).isEqualTo(body) + assertThat(server.requestCount).isEqualTo(1) + } + + @Test + fun `a create accepts 200 and 201`() = runTest { + server.enqueue(MockResponse.Builder().code(201).body("""{"identifier":"abc"}""").build()) + val created = client.createSettings(JsonObject(mapOf("a" to JsonPrimitive(1)))) + assertThat(created.response).isEqualTo(201) + assertThat(created.identifier).isEqualTo("abc") + + server.enqueue(MockResponse.Builder().code(200).body("""{"identifier":"def"}""").build()) + val updated = client.createSettings(JsonObject(mapOf("a" to JsonPrimitive(1)))) + assertThat(updated.response).isEqualTo(200) + assertThat(updated.identifier).isEqualTo("def") + } + + /** + * `identifier` coming back from a create is load bearing: `NSClientV3Plugin` stores it as the + * Nightscout id, and without one the record is uploaded again as a new document, forever. + */ + @Test + fun `a create returns the identifier from the body`() = runTest { + server.enqueue(MockResponse.Builder().code(201).body("""{"identifier":"1bebd84b-8d61-58b2"}""").build()) + + val response = client.createSettings(JsonObject(mapOf("a" to JsonPrimitive(1)))) + + assertThat(response.identifier).isEqualTo("1bebd84b-8d61-58b2") + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt new file mode 100644 index 000000000000..0000d197b4c4 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt @@ -0,0 +1,183 @@ +package app.aaps.core.nssdk.networking + +import android.content.Context +import app.aaps.core.nssdk.NSAndroidClientImpl +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import java.nio.file.Files + +/** + * Pins the exact URL every endpoint puts on the wire. + * + * This is the highest value test in the module, because a wrong URL does not fail - Nightscout + * answers **200 with the wrong rows**. Two of the details being pinned here are invisible unless you + * read the Retrofit annotations very carefully: + * + * 1. **Static query pairs live inside the `@GET` string**, not in `@Query` parameters. For example + * `v3/profile?sort${'$'}desc=date&limit=1`. A port that rebuilds the URL from the method parameters + * silently drops them - and losing `limit=1` there makes `LoadProfileStoreWorker` pick the + * **wrong profile store**, which means different basal, ISF and IC, with a normal looking log line. + * 2. **`${'$'}` must stay a literal `${'$'}`.** `date${'$'}gt`, `created_at${'$'}gt` and `sort${'$'}desc` are Nightscout API v3 + * operators, and Retrofit is told `encoded = true` so they pass through untouched. Most URL + * builders percent-encode `${'$'}` to `%24` by default, which changes the query rather than failing. + * + * The test drives a **real HTTP server on localhost** rather than a library specific fake, so this + * same file runs unchanged against Retrofit today and against Ktor after the port. That is the whole + * point: a fake that only exists after the swap could never prove anything about the behaviour + * before it. + * + * Only the request line is asserted. Whether the response parses is irrelevant here, so each call is + * wrapped in `runCatching`. + */ +class NsSdkUrlContractTest { + + private lateinit var server: MockWebServer + private lateinit var client: NSAndroidClientImpl + private lateinit var cacheDir: File + + /** + * A plain temp directory, not JUnit's `@TempDir`. + * + * Nothing closes the OkHttp client (there is no `close()` on `NSAndroidClient`), so its `Cache` + * keeps the `journal` file open and `@TempDir` fails the test on Windows while trying to delete + * it - the assertions pass, the cleanup does not. Deleting best effort here keeps the failure + * signal honest. When the disk cache goes away with the Ktor port, this can become `@TempDir`. + */ + @BeforeEach + fun setUp() { + cacheDir = Files.createTempDirectory("nssdk-test-cache").toFile() + server = MockWebServer() + server.start() + val context = mock { on { getCacheDir() } doReturn cacheDir } + client = NSAndroidClientImpl( + // Carries a scheme, so it is used as it stands and points at the local server. + baseUrl = server.url("/").toString().trimEnd('/'), + accessToken = "token", + context = context, + logging = false, + logger = { }, + dispatcher = Dispatchers.Unconfined + ) + } + + @AfterEach + fun tearDown() { + server.close() + cacheDir.deleteRecursively() // best effort, see setUp + } + + /** Runs [call], ignores how it ends, and returns the request line the server saw. */ + private suspend fun pathOf(body: String = """{"result":[]}""", call: suspend () -> Unit): String { + server.enqueue(MockResponse.Builder().code(200).body(body).build()) + runCatching { call() } + return server.takeRequest().target + } + + // ---------------------------------------------------------------- entries + + @Test + fun `entries endpoints keep their static query parts`() = runTest { + assertThat(pathOf { client.getSgvs() }) + .isEqualTo("/api/v3/entries?sort\$desc=date&type=sgv") + + assertThat(pathOf { client.getSgvsNewerThan(1700000000000, 250) }) + .isEqualTo("/api/v3/entries?sort=date&date\$gt=1700000000000&limit=250") + + assertThat(pathOf { client.getSgvsModifiedSince(1700000000000, 250) }) + .isEqualTo("/api/v3/entries/history/1700000000000?limit=250") + } + + /** `getSgvs()` deliberately sends no `limit` - the server default applies. */ + @Test + fun `getSgvs sends no limit`() = runTest { + assertThat(pathOf { client.getSgvs() }).doesNotContain("limit") + } + + // ---------------------------------------------------------------- treatments + + @Test + fun `treatments endpoints keep their static query parts`() = runTest { + assertThat(pathOf { client.getTreatmentsNewerThan("2024-01-01T00:00:00.000Z", 250) }) + .isEqualTo("/api/v3/treatments?sort=created_at&created_at\$gt=2024-01-01T00:00:00.000Z&limit=250") + + assertThat(pathOf { client.getTreatmentsModifiedSince(1700000000000, 250) }) + .isEqualTo("/api/v3/treatments/history/1700000000000?limit=250") + } + + // ---------------------------------------------------------------- profile + + /** + * The dangerous one. Without `sort${'$'}desc=date&limit=1` the server returns an unordered list and + * `LoadProfileStoreWorker` takes `profiles[profiles.size - 1]` from it. + */ + @Test + fun `getLastProfileStore keeps sort and limit`() = runTest { + assertThat(pathOf { client.getLastProfileStore() }) + .isEqualTo("/api/v3/profile?sort\$desc=date&limit=1") + } + + /** `limit=10` comes from a default on the interface and is invisible at the call site. */ + @Test + fun `getProfileModifiedSince sends the interface default limit`() = runTest { + assertThat(pathOf { client.getProfileModifiedSince(1700000000000) }) + .isEqualTo("/api/v3/profile/history/1700000000000?limit=10") + } + + // ---------------------------------------------------------------- settings + + @Test + fun `settings read endpoints`() = runTest { + assertThat(pathOf(body = """{"result":{}}""") { client.getSettings("aaps") }) + .isEqualTo("/api/v3/settings/aaps") + + assertThat(pathOf { client.getSettingsModifiedSince(1700000000000, 100) }) + .isEqualTo("/api/v3/settings/history/1700000000000?limit=100") + + assertThat(pathOf { client.searchSettings(100) }) + .isEqualTo("/api/v3/settings?limit=100") + } + + /** + * Absence of `permanent` is what makes a delete a soft delete (a tombstone). Sending + * `permanent=false` would be a different operation, so the parameter must be missing entirely. + */ + @Test + fun `a soft delete sends no permanent parameter`() = runTest { + val path = pathOf(body = """{}""") { client.deleteSettings("aaps_x") } + + assertThat(path).isEqualTo("/api/v3/settings/aaps_x") + assertThat(path).doesNotContain("permanent") + } + + @Test + fun `a permanent delete sends permanent true`() = runTest { + assertThat(pathOf(body = """{}""") { client.deleteSettingsPermanent("aaps_x") }) + .isEqualTo("/api/v3/settings/aaps_x?permanent=true") + } + + // ---------------------------------------------------------------- base url shapes + + /** A sub-path install must keep its sub-path on the data endpoints. */ + @Test + fun `a sub-path install keeps the sub-path`() { + assertThat(NetworkStackBuilder.toBaseUrl("host.com/ns")).isEqualTo("https://host.com/ns/api/") + assertThat(NetworkStackBuilder.toBaseUrl("host.com")).isEqualTo("https://host.com/api/") + assertThat(NetworkStackBuilder.toBaseUrl("host.com/")).isEqualTo("https://host.com/api/") + } + + /** An explicit scheme is honoured - this is what lets the tests above reach localhost. */ + @Test + fun `an explicit scheme is kept`() { + assertThat(NetworkStackBuilder.toBaseUrl("http://localhost:8080")).isEqualTo("http://localhost:8080/api/") + assertThat(NetworkStackBuilder.toBaseUrl("https://host.com")).isEqualTo("https://host.com/api/") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7358cd82ce5e..5722e7dd63b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -129,6 +129,7 @@ com-uber-rxdogtag2-rxdogtag = { group = "com.uber.rxdogtag2", name = "rxdogtag", com-squareup-leakcanary-android = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.14" } com-squareup-okhttp3-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } com-squareup-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +com-squareup-okhttp3-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver3-junit5", version.ref = "okhttp" } com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } com-squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } com-squareup-retrofit2-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } From ed9c87599fb554e50a1184b1a82979e10fa02f07 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 18:04:16 +0200 Subject: [PATCH 009/146] :core:nssdk ktor migration --- core/nssdk/build.gradle.kts | 12 +- .../aaps/core/nssdk/NSAndroidClientImpl.kt | 322 +++++++++--------- .../nssdk/exceptions/NightscoutException.kt | 17 +- .../core/nssdk/interfaces/NSAndroidClient.kt | 3 + .../nssdk/networking/NSAuthInterceptor.kt | 68 ---- .../nssdk/networking/NetworkStackBuilder.kt | 121 ------- .../core/nssdk/networking/NightscoutApi.kt | 285 ++++++++++++++++ .../NightscoutAuthRefreshService.kt | 16 - .../networking/NightscoutRemoteService.kt | 142 -------- .../app/aaps/core/nssdk/networking/NsAuth.kt | 106 ++++++ .../core/nssdk/networking/NsHttpResponse.kt | 59 ++++ .../core/nssdk/networking/NsKtorClient.kt | 90 +++++ .../app/aaps/core/nssdk/networking/NsUrl.kt | 26 ++ .../exceptions/NightscoutExceptionTest.kt | 69 ++++ .../nssdk/networking/NightscoutApiUrlTest.kt | 154 +++++++++ .../nssdk/networking/NsSdkAuthContractTest.kt | 10 - .../networking/NsSdkErrorBodyContractTest.kt | 10 - .../networking/NsSdkResponseContractTest.kt | 10 - .../networking/NsSdkStatusContractTest.kt | 29 +- .../nssdk/networking/NsSdkUrlContractTest.kt | 33 +- .../utils/ClientControlCryptoVectorsTest.kt | 143 ++++++++ gradle/libs.versions.toml | 6 + .../sync/nsclientV3/NSClientV3Plugin.kt | 4 +- .../sync/nsclientV3/workers/LoadBgWorker.kt | 13 +- .../workers/LoadTreatmentsWorker.kt | 8 +- 25 files changed, 1193 insertions(+), 563 deletions(-) delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NSAuthInterceptor.kt delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutAuthRefreshService.kt delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt create mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt create mode 100644 core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 6cdc0dc5e1a5..61dbefa542c5 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -11,12 +11,14 @@ android { } dependencies { - implementation(libs.com.squareup.retrofit2.retrofit) - // Official Retrofit 3 artifact, same group and version as the Gson converter it replaces. - // Goes away with Retrofit itself when the client moves to Ktor. - implementation(libs.com.squareup.retrofit2.converter.kotlinx.serialization) api(libs.com.squareup.okhttp3.okhttp) - api(libs.com.squareup.okhttp3.logging.interceptor) + + // Ktor on the OkHttp engine: reuses the OkHttp already in the app rather than adding a second + // HTTP stack. The engine becomes Darwin when this module builds for iOS. + api(libs.io.ktor.client.core) + implementation(libs.io.ktor.client.okhttp) + implementation(libs.io.ktor.client.content.negotiation) + implementation(libs.io.ktor.serialization.kotlinx.json) api(libs.kotlinx.datetime) // Test only: a real HTTP server on localhost, so the same characterization tests run against diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt index 741caa436e17..b1a3b3415221 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk -import android.content.Context import app.aaps.core.nssdk.exceptions.DateHeaderOutOfToleranceException import app.aaps.core.nssdk.exceptions.InvalidAccessTokenException import app.aaps.core.nssdk.exceptions.InvalidFormatNightscoutException @@ -25,11 +24,13 @@ import app.aaps.core.nssdk.mapper.toRemoteFood import app.aaps.core.nssdk.mapper.toRemoteTreatment import app.aaps.core.nssdk.mapper.toSgv import app.aaps.core.nssdk.mapper.toTreatment -import app.aaps.core.nssdk.networking.NetworkStackBuilder +import app.aaps.core.nssdk.networking.NsKtorClient +import app.aaps.core.nssdk.networking.NsUrl import app.aaps.core.nssdk.remotemodel.LastModified import app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus import app.aaps.core.nssdk.remotemodel.RemoteEntry import app.aaps.core.nssdk.remotemodel.RemoteFood +import app.aaps.core.nssdk.remotemodel.RemoteStatusResponse import app.aaps.core.nssdk.remotemodel.RemoteTreatment import app.aaps.core.nssdk.utils.retry import app.aaps.core.nssdk.utils.toNotNull @@ -38,7 +39,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import okhttp3.logging.HttpLoggingInterceptor /** * @@ -64,19 +64,28 @@ import okhttp3.logging.HttpLoggingInterceptor class NSAndroidClientImpl( baseUrl: String, accessToken: String, - context: Context, logging: Boolean, - logger: HttpLoggingInterceptor.Logger, + logger: (String) -> Unit, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : NSAndroidClient { - internal val api = NetworkStackBuilder.getApi( - baseUrl = baseUrl, - context = context, - accessToken = accessToken, + private val stack = NsKtorClient.stack( + baseUrl = NsUrl.toBaseUrl(baseUrl), + refreshToken = accessToken, logging = logging, logger = logger ) + + internal val api = stack.api + + /** + * Releases the HTTP engine. + * + * `NSClientV3Plugin` builds a new client whenever the URL, token or WebSocket setting changes, + * and the old one used to be dropped without being closed - a leak that was invisible with + * Retrofit but real with a Ktor engine holding connections. + */ + override fun close() = stack.close() override var lastStatus: Status? = null /* @@ -97,11 +106,28 @@ class NSAndroidClientImpl( // TODO: we need a minimum NightscoutVersion for APIv3. Add to documentation override suspend fun getVersion(): String = callWrapper(dispatcher) { - api.statusSimple().result!!.version + fetchStatus().version } override suspend fun getStatus(): Status = callWrapper(dispatcher) { - api.statusSimple().result!!.toLocal().also { lastStatus = it } + fetchStatus().toLocal().also { lastStatus = it } + } + + /** + * `v3/status`, or a [UnsuccessfulNightscoutException]. + * + * These two used to read `api.statusSimple().result!!`, which threw `retrofit2.HttpException` + * for a non-2xx and `NullPointerException` for a 200 without a `result` - neither of them a + * `NightscoutException`, and the first one impossible to keep once Retrofit is gone. Both now + * fail as the rest of the client does. The only caller, `LoadStatusWorker`, catches broad + * `Exception`, so nothing downstream notices the change. + */ + private suspend fun fetchStatus(): RemoteStatusResponse { + val response = api.statusSimple() + if (!response.isSuccessful) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) + return response.body()?.result + ?: throw UnsuccessfulNightscoutException("Status response has no result") } override suspend fun getLastModified(): LastModified = callWrapper(dispatcher) { @@ -109,8 +135,8 @@ class NSAndroidClientImpl( val response = api.lastModified() if (response.isSuccessful) { return@callWrapper response.body()?.result ?: throw UnsuccessfulNightscoutException("Unsuccessful") - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -120,13 +146,13 @@ class NSAndroidClientImpl( val response = api.getSgvs() if (response.isSuccessful) { return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = 0, values = response.body()?.result?.map(RemoteEntry::toSgv).toNotNull() // no calibrations: this endpoint is server-filtered to type=sgv ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -135,16 +161,15 @@ class NSAndroidClientImpl( val response = api.getSgvsModifiedSince(from, limit) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + val eTag = response.eTagAsLong() return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = eTag, values = response.body()?.result?.map(RemoteEntry::toSgv).toNotNull(), calibrations = response.body()?.result?.mapNotNull(RemoteEntry::toCalibrationMbg) ?: emptyList() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -154,13 +179,13 @@ class NSAndroidClientImpl( val response = api.getSgvsNewerThan(from, limit) if (response.isSuccessful) { return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = 0, values = response.body()?.result?.map(RemoteEntry::toSgv).toNotNull(), calibrations = response.body()?.result?.mapNotNull(RemoteEntry::toCalibrationMbg) ?: emptyList() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -171,31 +196,31 @@ class NSAndroidClientImpl( remoteEntry.app = "AAPS" val response = api.createEntry(remoteEntry) val responseBody = response.body() - val errorResponse = response.errorBody()?.string() - if (response.code() == 200 || response.code() == 201) { + val errorResponse = response.errorBody() + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = responseBody?.identifier, isDeduplication = responseBody?.isDeduplication == true, deduplicatedIdentifier = responseBody?.deduplicatedIdentifier, lastModified = responseBody?.lastModified ) - } else if (response.code() == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsSgvV3.utcOffset != 0L) { + } else if (response.code == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsSgvV3.utcOffset != 0L) { // Record can be originally uploaded without utcOffset // because utcOffset is mandatory and cannot be change, try 0 nsSgvV3.utcOffset = 0 return@callWrapper createSgv(nsSgvV3) - } else if (response.code() == 400 && errorResponse?.contains("cannot be modified by the client") == true) { + } else if (response.code == 400 && errorResponse?.contains("cannot be modified by the client") == true) { // there is different field to field in AAPS // not possible to upload return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ?: response.message() ) @@ -213,22 +238,22 @@ class NSAndroidClientImpl( val response = if (nsSgvV3.isValid) api.updateEntry(remoteEntry, identifier) else api.deleteEntry(identifier) - if (response.isSuccessful || response.code() == 404) { // OK or not found + if (response.isSuccessful || response.code == 404) { // OK or not found return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun createCalibration(nsMbgV3: NSMbgV3): CreateUpdateResponse = callWrapper(dispatcher) { @@ -237,28 +262,28 @@ class NSAndroidClientImpl( remoteEntry.app = "AAPS" val response = api.createEntry(remoteEntry) val responseBody = response.body() - val errorResponse = response.errorBody()?.string() - if (response.code() == 200 || response.code() == 201) { + val errorResponse = response.errorBody() + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = responseBody?.identifier, isDeduplication = responseBody?.isDeduplication == true, deduplicatedIdentifier = responseBody?.deduplicatedIdentifier, lastModified = responseBody?.lastModified ) - } else if (response.code() == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsMbgV3.utcOffset != 0L) { + } else if (response.code == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsMbgV3.utcOffset != 0L) { nsMbgV3.utcOffset = 0 return@callWrapper createCalibration(nsMbgV3) - } else if (response.code() == 400 && errorResponse?.contains("cannot be modified by the client") == true) { + } else if (response.code == 400 && errorResponse?.contains("cannot be modified by the client") == true) { // there is different field to field in AAPS, not possible to upload return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ?: response.message() ) @@ -276,22 +301,22 @@ class NSAndroidClientImpl( val response = if (nsMbgV3.isValid) api.updateEntry(remoteEntry, identifier) else api.deleteEntry(identifier) - if (response.isSuccessful || response.code() == 404) { // OK or not found + if (response.isSuccessful || response.code == 404) { // OK or not found return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun getTreatmentsNewerThan(createdAt: String, limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { @@ -299,12 +324,12 @@ class NSAndroidClientImpl( val response = api.getTreatmentsNewerThan(createdAt, limit) if (response.isSuccessful) { return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = 0, values = response.body()?.result?.map(RemoteTreatment::toTreatment).toNotNull() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -313,14 +338,13 @@ class NSAndroidClientImpl( val response = api.getTreatmentsModifiedSince(from, limit) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + val eTag = response.eTagAsLong() return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), lastServerModified = eTag, values = response.body()?.result?.map + code = response.code, lastServerModified = eTag, values = response.body()?.result?.map (RemoteTreatment::toTreatment).toNotNull() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -330,8 +354,8 @@ class NSAndroidClientImpl( val response = api.getDeviceStatusModifiedSince(from) if (response.isSuccessful) { return@callWrapper response.body()?.result?.map(RemoteDeviceStatus::toNSDeviceStatus).toNotNull() - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -341,9 +365,9 @@ class NSAndroidClientImpl( nsDeviceStatus.app = "AAPS" val response = api.createDeviceStatus(nsDeviceStatus.toRemoteDeviceStatus()) if (response.isSuccessful) { - if (response.code() == 200 || response.code() == 201) { + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, @@ -351,9 +375,9 @@ class NSAndroidClientImpl( ) } else throw UnknownResponseNightscoutException("Unsuccessful") } else return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } @@ -362,31 +386,31 @@ class NSAndroidClientImpl( val remoteTreatment = nsTreatment.toRemoteTreatment() ?: throw InvalidFormatNightscoutException("Invalid format") remoteTreatment.app = "AAPS" val response = api.createTreatment(remoteTreatment) - val errorResponse = response.errorBody()?.string() - if (response.code() == 200 || response.code() == 201) { + val errorResponse = response.errorBody() + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication == true, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) - } else if (response.code() == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsTreatment.utcOffset != 0L) { + } else if (response.code == 400 && errorResponse?.contains("Bad or missing utcOffset field") == true && nsTreatment.utcOffset != 0L) { // Record can be originally uploaded without utcOffset // because utcOffset is mandatory and cannot be change, try 0 nsTreatment.utcOffset = 0 return@callWrapper createTreatment(nsTreatment) - } else if (response.code() == 400 && errorResponse?.contains("cannot be modified by the client") == true) { + } else if (response.code == 400 && errorResponse?.contains("cannot be modified by the client") == true) { // there is different field to field in AAPS // not possible to upload return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, errorResponse = errorResponse ?: response.message() ) @@ -404,22 +428,22 @@ class NSAndroidClientImpl( val response = if (nsTreatment.isValid) api.updateTreatment(remoteTreatment, identifier) else api.deleteTreatment(identifier) - if (response.isSuccessful || response.code() == 404) { // OK or not found + if (response.isSuccessful || response.code == 404) { // OK or not found return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun getFoods(limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { @@ -427,12 +451,12 @@ class NSAndroidClientImpl( val response = api.getFoods(limit) if (response.isSuccessful) { return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = 0, values = response.body()?.result?.map(RemoteFood::toNSFood).toNotNull() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -441,8 +465,7 @@ class NSAndroidClientImpl( override suspend fun getFoodsModifiedSince(from: Long, limit: Int): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.getFoodsModifiedSince(from, limit) - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() ?: throw UnsuccessfulNightscoutException() + val eTag = response.eTagAsLong() ?: throw UnsuccessfulNightscoutException() if (response.isSuccessful) { return@callWrapper NSAndroidClient.ReadResponse(eTag, response.body()?.result?.map(RemoteFood::toNSFood).toNotNull()) } else { @@ -456,23 +479,23 @@ class NSAndroidClientImpl( remoteFood.app = "AAPS" val response = api.createFood(remoteFood) if (response.isSuccessful) { - if (response.code() == 200 || response.code() == 201) { + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) } else throw UnsuccessfulNightscoutException("Unsuccessful") - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun updateFood(nsFood: NSFood): CreateUpdateResponse = callWrapper(dispatcher) { @@ -482,22 +505,22 @@ class NSAndroidClientImpl( val response = if (nsFood.isValid) api.updateFood(remoteFood, identifier) else api.deleteFood(identifier) - if (response.isSuccessful || response.code() == 404) { // OK or not found + if (response.isSuccessful || response.code == 404) { // OK or not found return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun createProfileStore(remoteProfileStore: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { @@ -506,34 +529,33 @@ class NSAndroidClientImpl( val stamped = JsonObject(remoteProfileStore + ("app" to JsonPrimitive("AAPS"))) val response = api.createProfile(stamped) if (response.isSuccessful) { - if (response.code() == 200 || response.code() == 201) { + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) } else throw UnsuccessfulNightscoutException("Unsuccessful") - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun getLastProfileStore(): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { val response = api.getLastProfile() if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() - return@callWrapper NSAndroidClient.ReadResponse(code = response.raw().networkResponse?.code ?: response.code(), lastServerModified = eTag, values = response.body()?.result.toNotNull()) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + val eTag = response.eTagAsLong() + return@callWrapper NSAndroidClient.ReadResponse(code = response.code, lastServerModified = eTag, values = response.body()?.result.toNotNull()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -542,11 +564,10 @@ class NSAndroidClientImpl( val response = api.getProfileModifiedSince(from) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() - return@callWrapper NSAndroidClient.ReadResponse(code = response.raw().networkResponse?.code ?: response.code(), lastServerModified = eTag, values = response.body()?.result.toNotNull()) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + val eTag = response.eTagAsLong() + return@callWrapper NSAndroidClient.ReadResponse(code = response.code, lastServerModified = eTag, values = response.body()?.result.toNotNull()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -555,17 +576,16 @@ class NSAndroidClientImpl( val response = api.getSetting(identifier) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + val eTag = response.eTagAsLong() return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = eTag, values = response.body()?.result ) - } else if (response.code() == 404) { + } else if (response.code == 404) { return@callWrapper NSAndroidClient.ReadResponse(code = 404, lastServerModified = null, values = null) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -574,15 +594,14 @@ class NSAndroidClientImpl( val response = api.getSettingsModifiedSince(from, limit) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + val eTag = response.eTagAsLong() return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = eTag, values = response.body()?.result.toNotNull() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -591,15 +610,14 @@ class NSAndroidClientImpl( val response = api.searchSettings(limit) if (response.isSuccessful) { - val eTagString = response.headers()["ETag"] - val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + val eTag = response.eTagAsLong() return@callWrapper NSAndroidClient.ReadResponse( - code = response.raw().networkResponse?.code ?: response.code(), + code = response.code, lastServerModified = eTag, values = response.body()?.result.toNotNull() ) - } else if (response.code() in 400..499) - throw InvalidParameterNightscoutException(response.errorBody()?.string() ?: response.message()) + } else if (response.code in 400..499) + throw InvalidParameterNightscoutException(response.errorBody() ?: response.message()) else throw UnsuccessfulNightscoutException("Unsuccessful") } @@ -610,29 +628,29 @@ class NSAndroidClientImpl( val stamped = JsonObject(settings + ("app" to JsonPrimitive("AAPS"))) val response = api.createSetting(stamped) if (response.isSuccessful) { - if (response.code() == 200 || response.code() == 201) { + if (response.code == 200 || response.code == 201) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) } else throw UnsuccessfulNightscoutException("Unsuccessful") - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun patchSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { val response = api.patchSetting(settings, identifier) - if (response.code() == 404) { + if (response.code == 404) { return@callWrapper CreateUpdateResponse( response = 404, identifier = null, @@ -642,20 +660,20 @@ class NSAndroidClientImpl( ) } else if (response.isSuccessful) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication == true, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun updateSettings(identifier: String, settings: JsonObject): CreateUpdateResponse = callWrapper(dispatcher) { @@ -663,20 +681,20 @@ class NSAndroidClientImpl( val response = api.updateSetting(settings, identifier) if (response.isSuccessful) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = response.body()?.identifier, isDeduplication = response.body()?.isDeduplication == true, deduplicatedIdentifier = response.body()?.deduplicatedIdentifier, lastModified = response.body()?.lastModified ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } override suspend fun deleteSettings(identifier: String): CreateUpdateResponse = deleteSettingsInternal(identifier, permanent = false) @@ -687,22 +705,22 @@ class NSAndroidClientImpl( private suspend fun deleteSettingsInternal(identifier: String, permanent: Boolean): CreateUpdateResponse = callWrapper(dispatcher) { val response = api.deleteSetting(identifier, if (permanent) true else null) - if (response.isSuccessful || response.code() == 404) { + if (response.isSuccessful || response.code == 404) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, isDeduplication = false, deduplicatedIdentifier = null, lastModified = null ) - } else if (response.code() in 400..499) { + } else if (response.code in 400..499) { return@callWrapper CreateUpdateResponse( - response = response.code(), + response = response.code, identifier = null, - errorResponse = response.errorBody()?.string() ?: response.message() + errorResponse = response.errorBody() ?: response.message() ) } else - throw UnsuccessfulNightscoutException(response.errorBody()?.string() ?: response.message()) + throw UnsuccessfulNightscoutException(response.errorBody() ?: response.message()) } private suspend fun callWrapper(dispatcher: CoroutineDispatcher, block: suspend () -> T): T = diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt index a0c0fceab296..845355bd1b60 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt @@ -1,5 +1,20 @@ package app.aaps.core.nssdk.exceptions -import java.io.IOException +import kotlinx.io.IOException +/** + * Base class for everything this client throws. + * + * The base is **kotlinx-io**'s `IOException`, not `java.io.IOException`, because the latter is a JVM + * type and would keep this module off iOS. On the JVM kotlinx-io declares it as + * `actual typealias IOException = java.io.IOException`, so the two are the *same class* there and + * existing `catch (e: java.io.IOException)` blocks keep catching - `PairingOfferFetcher` and + * `PairingOfferPublisher` in `:plugins:sync` rely on that, and `deleteOffer` in particular must + * never throw, because a pairing offer left on the server keeps a PIN brute-force window open. + * + * `NightscoutExceptionTest` asserts the identity per subclass rather than trusting it: a hand rolled + * `expect class` would compile just as happily and silently turn those catches into non-catches. + * + * kotlinx-io arrives transitively with Ktor, so this costs no new dependency. + */ abstract class NightscoutException(message: String, cause: Throwable? = null) : IOException(message, cause) \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt index c40eaa5a4819..383a3fd12a86 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt @@ -66,6 +66,9 @@ interface NSAndroidClient { suspend fun updateSettings(identifier: String, settings: JsonObject): CreateUpdateResponse suspend fun deleteSettings(identifier: String): CreateUpdateResponse + /** Releases the HTTP engine. Call when the client is replaced or no longer needed. */ + fun close() + /** Hard delete via NS `?permanent=true` — removes the doc instead of soft-deleting (tombstoning) it. */ suspend fun deleteSettingsPermanent(identifier: String): CreateUpdateResponse } diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NSAuthInterceptor.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NSAuthInterceptor.kt deleted file mode 100644 index 3b8d36768f8d..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NSAuthInterceptor.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.aaps.core.nssdk.networking - -import app.aaps.core.nssdk.exceptions.DateHeaderOutOfToleranceException -import app.aaps.core.nssdk.exceptions.InvalidAccessTokenException -import app.aaps.core.nssdk.networking.Status.MESSAGE_DATE_HEADER_OUT_OF_TOLERANCE -import app.aaps.core.nssdk.remotemodel.RemoteAuthResponse -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import retrofit2.Retrofit -import java.lang.System.currentTimeMillis - -internal class NSAuthInterceptor(private val refreshToken: String, private val retrofit: Retrofit) : - Interceptor { - - private var jwtToken = "" // the actual Bearer token - - @Suppress("MagicNumber") - override fun intercept(chain: Interceptor.Chain): Response { - - val originalRequest = chain.request() - val authenticationRequest = requestWithBearer(originalRequest) - val initialResponse = chain.proceed(authenticationRequest) - - return when (initialResponse.code) { - 403, 401 -> refreshTokenAndRetry(originalRequest, initialResponse, chain) - else -> initialResponse - } - } - - private fun requestWithBearer(originalRequest: Request): Request = originalRequest.newBuilder() - .addHeader("Date", currentTimeMillis().toString()) - .addHeader("Authorization", "Bearer $jwtToken") - .build() - - @Suppress("MagicNumber") - private fun refreshTokenAndRetry( - originalRequest: Request, - initialResponse: Response, - chain: Interceptor.Chain - ): Response { - - testCanRefresh(initialResponse) - - val authResponseResponse: retrofit2.Response? = retrofit - .create(NightscoutAuthRefreshService::class.java) - .refreshToken(refreshToken) - .execute() - - return when { - authResponseResponse == null -> initialResponse - authResponseResponse.code() in listOf(401, 403) -> throw InvalidAccessTokenException("Invalid access token") - authResponseResponse.code() != 200 -> initialResponse - else -> { - authResponseResponse.body()?.token?.let { jwtToken = it } - val newAuthenticationRequest = requestWithBearer(originalRequest) - chain.proceed(newAuthenticationRequest) - } - } - } - - private fun testCanRefresh(initialResponse: Response) { - // Todo: use proper reason code once it is supplied by remote - if (initialResponse.body.string().contains(MESSAGE_DATE_HEADER_OUT_OF_TOLERANCE)) { - throw DateHeaderOutOfToleranceException("Data header out of tolerance") - } - } -} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt deleted file mode 100644 index 9ea0628b5862..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NetworkStackBuilder.kt +++ /dev/null @@ -1,121 +0,0 @@ -package app.aaps.core.nssdk.networking - -import android.content.Context -import app.aaps.core.nssdk.nsSdkJson -import okhttp3.Cache -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.kotlinx.serialization.asConverterFactory -import java.util.concurrent.TimeUnit - -internal object NetworkStackBuilder { - - /** - * Turns what the caller supplies into the Retrofit base URL. - * - * Production callers pass a bare host with an optional sub-path - `NSClientV3Plugin.setClient` - * strips the scheme first - so `host.com` and `host.com/ns` behave exactly as before. - * - * An input that already carries a scheme is used as it stands. That is what lets a unit test - * point the client at `http://localhost:`, and it also stops a stored `http://host` from - * turning into `https://http://host/api/`, which is what the old string concatenation produced - * (the caller only strips `https://`). - */ - internal fun toBaseUrl(hostOrUrl: String): String { - val trimmed = hostOrUrl.trimEnd('/') - val withScheme = if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) trimmed else "https://$trimmed" - return "$withScheme/api/" - } - - @JvmSynthetic - internal fun getApi( - baseUrl: String, - context: Context, - accessToken: String, // refresh token - logging: Boolean, - logger: HttpLoggingInterceptor.Logger - ): NightscoutRemoteService = getRetrofit( - baseUrl = baseUrl, - context = context, - refreshToken = accessToken, - logging = logging, - logger = logger - ).create(NightscoutRemoteService::class.java) - - private fun getRetrofit( - baseUrl: String, - context: Context, - refreshToken: String, - logging: Boolean, - logger: HttpLoggingInterceptor.Logger - ): Retrofit = - Retrofit.Builder() - .baseUrl(toBaseUrl(baseUrl)) - .client( - getOkHttpClient( - context = context, - logging = logging, - refreshToken = refreshToken, - authRefreshRetrofit = getAuthRefreshRetrofit(baseUrl, context, logging, logger), - logger = logger - ) - ) - .addConverterFactory(converterFactory) - .build() - - private fun getAuthRefreshRetrofit( - baseUrl: String, - context: Context, - logging: Boolean, - logger: HttpLoggingInterceptor.Logger - ): Retrofit = - Retrofit.Builder() - .baseUrl(toBaseUrl(baseUrl)) - .client(getAuthRefreshOkHttpClient(context = context, logging = logging, logger = logger)) - .addConverterFactory(converterFactory) - .build() - - private fun getOkHttpClient( - context: Context, - logging: Boolean, - refreshToken: String, - authRefreshRetrofit: Retrofit, - logger: HttpLoggingInterceptor.Logger - ): OkHttpClient = OkHttpClient.Builder().run { - addInterceptor(NSAuthInterceptor(refreshToken, authRefreshRetrofit)) - commonOkHttpSetup(logging, context, logger) - } - - private fun getAuthRefreshOkHttpClient( - context: Context, - logging: Boolean, - logger: HttpLoggingInterceptor.Logger - ): OkHttpClient = OkHttpClient.Builder().run { commonOkHttpSetup(logging, context, logger) } - - private fun OkHttpClient.Builder.commonOkHttpSetup( - logging: Boolean, - context: Context, - logger: HttpLoggingInterceptor.Logger - ): OkHttpClient { - if (logging) { - addNetworkInterceptor( - HttpLoggingInterceptor(logger).also { it.level = HttpLoggingInterceptor.Level.BODY } - ) - } - cache(Cache(context.cacheDir, OK_HTTP_CACHE_SIZE)) - readTimeout(OK_HTTP_READ_TIMEOUT, TimeUnit.MILLISECONDS) - writeTimeout(OK_HTTP_WRITE_TIMEOUT, TimeUnit.MILLISECONDS) - return build() - } - - // The schema-less documents (profiles, settings) are carried as kotlinx JsonObject. Gson needed a - // registered type adapter to build those; kotlinx reads JsonObject natively, so the adapter and - // the Gson instance behind it are gone. - private val converterFactory = nsSdkJson.asConverterFactory("application/json".toMediaType()) - - private const val OK_HTTP_CACHE_SIZE = 10L * 1024 * 1024 - private const val OK_HTTP_READ_TIMEOUT = 60L * 1000 - private const val OK_HTTP_WRITE_TIMEOUT = 60L * 1000 -} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt new file mode 100644 index 000000000000..ebf77d03c316 --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt @@ -0,0 +1,285 @@ +package app.aaps.core.nssdk.networking + +import app.aaps.core.nssdk.remotemodel.LastModified +import app.aaps.core.nssdk.remotemodel.NSResponse +import app.aaps.core.nssdk.remotemodel.RemoteCreateUpdateResponse +import app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus +import app.aaps.core.nssdk.remotemodel.RemoteEntry +import app.aaps.core.nssdk.remotemodel.RemoteFood +import app.aaps.core.nssdk.remotemodel.RemoteStatusResponse +import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import io.ktor.client.HttpClient +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.URLBuilder +import io.ktor.http.appendPathSegments +import io.ktor.http.contentType +import io.ktor.http.takeFrom +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonObject + +/** + * The Nightscout API v3 endpoints, on Ktor. + * + * Replaces the Retrofit interface. The method names, parameters and order are kept identical so the + * change in `NSAndroidClientImpl` is a type swap rather than a rewrite - the point is that the + * contract tests stay meaningful, not that the code gets prettier. + * + * ### Two URL rules that matter + * + * **Query keys keep their `$`.** `date$gt`, `created_at$gt` and `sort$desc` are Nightscout operators, + * and Retrofit was told `encoded = true` so they went out untouched. Ktor percent-encodes by default, + * and `date%24gt` is simply a different, unknown filter - Nightscout ignores it and answers **200 + * with unfiltered rows**. That is why every query below goes through [URLBuilder.encodedParameters] + * rather than `parameters`. + * + * **Path segments are encoded.** Identifiers come from the server and may contain characters that + * would otherwise create a new path segment, so [appendPathSegments] (which encodes) is used, exactly + * as Retrofit's `@Path` did by default. + * + * `NsSdkUrlContractTest` pins the resulting URL for every endpoint, as a literal string. + */ +internal class NightscoutApi( + private val client: HttpClient, + /** Fully formed, e.g. `https://host/api/` - see `NsUrl.toBaseUrl`. */ + private val baseUrl: String +) { + + // ---------------------------------------------------------------- plumbing + + private fun HttpRequestBuilder.nsUrl(vararg segments: String, query: URLBuilder.() -> Unit = {}) { + url { + takeFrom(baseUrl) + // encodeSlash = true so an identifier containing "/" stays ONE segment and addresses one + // document, instead of silently becoming a different path. Retrofit's @Path did this by + // default (it produced "a%2Fb"); Ktor's default does not, so it has to be asked for. + appendPathSegments(segments.toList(), encodeSlash = true) + query() + } + } + + /** + * Issues the request and reads the body **once**, whatever the status. + * + * See [NsHttpResponse] - the client inspects error bodies as raw text, so the body cannot be + * read lazily or only on success. + */ + private suspend fun call( + method: HttpMethod, + deserializer: DeserializationStrategy, + block: HttpRequestBuilder.() -> Unit + ): NsHttpResponse { + val response = client.request { + this.method = method + block() + } + return NsHttpResponse( + code = response.status.value, + eTagHeader = response.headers["ETag"], + bodyText = response.bodyAsText(), + deserializer = deserializer + ) + } + + private inline fun HttpRequestBuilder.jsonBody(value: T) { + contentType(ContentType.Application.Json) + setBody(value) + } + + // ---------------------------------------------------------------- status + + suspend fun statusSimple() = + call(HttpMethod.Get, NSResponse.serializer(RemoteStatusResponse.serializer())) { nsUrl("v3", "status") } + + suspend fun lastModified() = + call(HttpMethod.Get, NSResponse.serializer(LastModified.serializer())) { nsUrl("v3", "lastModified") } + + // ---------------------------------------------------------------- entries + + private val entryListSerializer = NSResponse.serializer(ListSerializer(RemoteEntry.serializer())) + + suspend fun getSgvs() = call(HttpMethod.Get, entryListSerializer) { + nsUrl("v3", "entries") { + encodedParameters.append("sort\$desc", "date") + encodedParameters.append("type", "sgv") + } + } + + suspend fun getSgvsNewerThan(date: Long, limit: Int) = call(HttpMethod.Get, entryListSerializer) { + nsUrl("v3", "entries") { + encodedParameters.append("sort", "date") + encodedParameters.append("date\$gt", date.toString()) + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun getSgvsModifiedSince(from: Long, limit: Int) = call(HttpMethod.Get, entryListSerializer) { + nsUrl("v3", "entries", "history", from.toString()) { + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun createEntry(remoteEntry: RemoteEntry) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "entries"); jsonBody(remoteEntry) + } + + suspend fun updateEntry(remoteEntry: RemoteEntry, identifier: String) = + call(HttpMethod.Patch, NSResponse.serializer(RemoteCreateUpdateResponse.serializer())) { + nsUrl("v3", "entries", identifier); jsonBody(remoteEntry) + } + + suspend fun deleteEntry(identifier: String) = + call(HttpMethod.Delete, NSResponse.serializer(RemoteCreateUpdateResponse.serializer())) { + nsUrl("v3", "entries", identifier) + } + + // ---------------------------------------------------------------- treatments + + private val treatmentListSerializer = NSResponse.serializer(ListSerializer(RemoteTreatment.serializer())) + + suspend fun getTreatmentsNewerThan(createdAt: String, limit: Int) = call(HttpMethod.Get, treatmentListSerializer) { + nsUrl("v3", "treatments") { + encodedParameters.append("sort", "created_at") + encodedParameters.append("created_at\$gt", createdAt) + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun getTreatmentsModifiedSince(from: Long, limit: Int) = call(HttpMethod.Get, treatmentListSerializer) { + nsUrl("v3", "treatments", "history", from.toString()) { + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun createTreatment(remoteTreatment: RemoteTreatment) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "treatments"); jsonBody(remoteTreatment) + } + + suspend fun updateTreatment(remoteTreatment: RemoteTreatment, identifier: String) = + call(HttpMethod.Patch, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "treatments", identifier); jsonBody(remoteTreatment) + } + + suspend fun deleteTreatment(identifier: String) = + call(HttpMethod.Delete, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "treatments", identifier) + } + + // ---------------------------------------------------------------- device status + + suspend fun createDeviceStatus(remoteDeviceStatus: RemoteDeviceStatus) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "devicestatus"); jsonBody(remoteDeviceStatus) + } + + suspend fun getDeviceStatusModifiedSince(from: Long) = + call(HttpMethod.Get, NSResponse.serializer(ListSerializer(RemoteDeviceStatus.serializer()))) { + nsUrl("v3", "devicestatus", "history", from.toString()) + } + + // ---------------------------------------------------------------- food + + suspend fun getFoods(limit: Int) = + call(HttpMethod.Get, NSResponse.serializer(ListSerializer(RemoteFood.serializer()))) { + nsUrl("v3", "food") { encodedParameters.append("limit", limit.toString()) } + } + + suspend fun createFood(remoteFood: RemoteFood) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "food"); jsonBody(remoteFood) + } + + /** + * Food update and delete **deliberately do not send a request. Do not "fix" these.** + * + * The Retrofit versions declared an `{identifier}` parameter that was missing from the path, so + * Retrofit refused to build them and no request was ever made - the throw is swallowed by the + * broad catch in `NSClientV3Plugin`, and food edits have never synced. The matching endpoint is + * broken on the Nightscout side, so sending would not help. + * + * Writing the "obvious" URL here would turn "never sends" into `PATCH /api/v3/food` and + * `DELETE /api/v3/food` **with no identifier** - a request against the whole collection on a + * live Nightscout. So the local failure is reproduced on purpose, keeping today's behaviour. + * When Nightscout supports these, change both sides together. + */ + @Suppress("UNUSED_PARAMETER") + suspend fun updateFood(remoteFood: RemoteFood, identifier: String): NsHttpResponse = + error("v3/food update is not supported by Nightscout - no request is sent, see NightscoutApi.updateFood") + + @Suppress("UNUSED_PARAMETER") + suspend fun deleteFood(identifier: String): NsHttpResponse = + error("v3/food delete is not supported by Nightscout - no request is sent, see NightscoutApi.updateFood") + + // ---------------------------------------------------------------- profile + + private val jsonObjectListSerializer = NSResponse.serializer(ListSerializer(JsonObject.serializer())) + + suspend fun getProfileModifiedSince(from: Long, limit: Int = 10) = call(HttpMethod.Get, jsonObjectListSerializer) { + nsUrl("v3", "profile", "history", from.toString()) { + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun getLastProfile() = call(HttpMethod.Get, jsonObjectListSerializer) { + nsUrl("v3", "profile") { + encodedParameters.append("sort\$desc", "date") + encodedParameters.append("limit", "1") + } + } + + suspend fun createProfile(profile: JsonObject) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "profile"); jsonBody(profile) + } + + // ---------------------------------------------------------------- settings + + suspend fun getSetting(identifier: String) = + call(HttpMethod.Get, NSResponse.serializer(JsonObject.serializer())) { + nsUrl("v3", "settings", identifier) + } + + suspend fun getSettingsModifiedSince(from: Long, limit: Int = 100) = call(HttpMethod.Get, jsonObjectListSerializer) { + nsUrl("v3", "settings", "history", from.toString()) { + encodedParameters.append("limit", limit.toString()) + } + } + + suspend fun searchSettings(limit: Int = 100) = call(HttpMethod.Get, jsonObjectListSerializer) { + nsUrl("v3", "settings") { encodedParameters.append("limit", limit.toString()) } + } + + suspend fun createSetting(settings: JsonObject) = + call(HttpMethod.Post, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "settings"); jsonBody(settings) + } + + suspend fun patchSetting(settings: JsonObject, identifier: String) = + call(HttpMethod.Patch, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "settings", identifier); jsonBody(settings) + } + + /** PUT (NS3 "UPDATE") = upsert. Replaces an existing doc, or inserts if absent. */ + suspend fun updateSetting(settings: JsonObject, identifier: String) = + call(HttpMethod.Put, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "settings", identifier); jsonBody(settings) + } + + /** + * [permanent] `null` -> soft delete (tombstone); `true` -> NS `?permanent=true` hard delete. + * A null must send **no parameter at all** - `permanent=false` would be a different request. + */ + suspend fun deleteSetting(identifier: String, permanent: Boolean?) = + call(HttpMethod.Delete, RemoteCreateUpdateResponse.serializer()) { + nsUrl("v3", "settings", identifier) { + permanent?.let { encodedParameters.append("permanent", it.toString()) } + } + } +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutAuthRefreshService.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutAuthRefreshService.kt deleted file mode 100644 index 7d0b88ab3694..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutAuthRefreshService.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.aaps.core.nssdk.networking - -import app.aaps.core.nssdk.remotemodel.RemoteAuthResponse -import retrofit2.Call -import retrofit2.http.GET -import retrofit2.http.Path - -/** - * Created by adrian on 2019-01-04. - */ - -internal interface NightscoutAuthRefreshService { - - @GET("/api/v2/authorization/request/{refreshToken}") - fun refreshToken(@Path("refreshToken") refreshToken: String): Call -} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt deleted file mode 100644 index 3a1522721dce..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutRemoteService.kt +++ /dev/null @@ -1,142 +0,0 @@ -package app.aaps.core.nssdk.networking - -import app.aaps.core.nssdk.remotemodel.LastModified -import app.aaps.core.nssdk.remotemodel.NSResponse -import app.aaps.core.nssdk.remotemodel.RemoteCreateUpdateResponse -import app.aaps.core.nssdk.remotemodel.RemoteDeviceStatus -import app.aaps.core.nssdk.remotemodel.RemoteEntry -import app.aaps.core.nssdk.remotemodel.RemoteFood -import app.aaps.core.nssdk.remotemodel.RemoteStatusResponse -import app.aaps.core.nssdk.remotemodel.RemoteTreatment -import kotlinx.serialization.json.JsonObject -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.DELETE -import retrofit2.http.GET -import retrofit2.http.PATCH -import retrofit2.http.POST -import retrofit2.http.PUT -import retrofit2.http.Path -import retrofit2.http.Query - -/** - * Created by adrian on 2019-12-23. - * - * https://github.com/nightscout/cgm-remote-monitor/blob/master/lib/api3/doc/tutorial.md - * - */ - -internal interface NightscoutRemoteService { - - @GET("v3/status") - // used to get the raw response for more error checking. E.g. to give the user better feedback after new settings. - suspend fun statusVerbose(): Response> - - @GET("v3/status") - suspend fun statusSimple(): NSResponse - - @GET("v3/lastModified") - suspend fun lastModified(): Response> - - @GET("v3/entries?sort\$desc=date&type=sgv") - suspend fun getSgvs(): Response>> - - @GET("v3/entries?sort=date") - suspend fun getSgvsNewerThan(@Query(value = "date\$gt", encoded = true) date: Long, @Query("limit") limit: Int): Response>> - - @GET("v3/entries/history/{from}") - suspend fun getSgvsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int): Response>> - - @POST("v3/entries") - suspend fun createEntry(@Body remoteEntry: RemoteEntry): Response - - @PATCH("v3/entries/{identifier}") - suspend fun updateEntry(@Body remoteEntry: RemoteEntry, @Path("identifier") identifier: String): Response> - - @DELETE("v3/entries/{identifier}") - suspend fun deleteEntry(@Path("identifier") identifier: String): Response> - - @GET("v3/treatments?sort=created_at") - suspend fun getTreatmentsNewerThan(@Query(value = "created_at\$gt", encoded = true) createdAt: String, @Query("limit") limit: Int): Response>> - - @GET("v3/treatments/history/{from}") - suspend fun getTreatmentsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int): Response>> - - @POST("v3/treatments") - suspend fun createTreatment(@Body remoteTreatment: RemoteTreatment): Response - - @PATCH("v3/treatments/{identifier}") - suspend fun updateTreatment(@Body remoteTreatment: RemoteTreatment, @Path("identifier") identifier: String): Response - - @DELETE("v3/treatments/{identifier}") - suspend fun deleteTreatment(@Path("identifier") identifier: String): Response - - @POST("v3/devicestatus") - suspend fun createDeviceStatus(@Body remoteDeviceStatus: RemoteDeviceStatus): Response - - @GET("v3/devicestatus/history/{from}") - suspend fun getDeviceStatusModifiedSince(@Path("from") from: Long): Response>> - - @GET("v3/food") - suspend fun getFoods(@Query("limit") limit: Int): Response>> - - /* - @GET("v3/food/history/{from}") - suspend fun getFoodsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int): Response>> - */ - @POST("v3/food") - suspend fun createFood(@Body remoteFood: RemoteFood): Response - - /** - * Update and delete for food are **left as they are on purpose. Do not "fix" the path.** - * - * The `{identifier}` placeholder is missing from the path, so Retrofit rejects these methods - * when it builds the request factory and **no request is ever sent** - the throw is swallowed by - * the broad catch in `NSClientV3Plugin`. Food edits therefore do not sync, and that is the - * current, accepted state: the matching endpoint is broken on the Nightscout side, so making - * AAPS send the request would not help. - * - * This matters for the move to Ktor. A hand-written URL would silently turn "never sends" - * into `PATCH /api/v3/food` and `DELETE /api/v3/food` with no identifier - a request against the - * whole collection on a live Nightscout. Keep these failing locally until Nightscout supports - * them, then change both sides together. - */ - @PATCH("v3/food") - suspend fun updateFood(@Body remoteFood: RemoteFood, @Path("identifier") identifier: String): Response - - @DELETE("v3/food") - suspend fun deleteFood(@Path("identifier") identifier: String): Response - - @GET("v3/profile/history/{from}") - suspend fun getProfileModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 10): Response>> - - @GET("v3/profile?sort\$desc=date&limit=1") - suspend fun getLastProfile(): Response>> - - @POST("v3/profile") - suspend fun createProfile(@Body profile: JsonObject): Response - - @GET("v3/settings/{identifier}") - suspend fun getSetting(@Path("identifier") identifier: String): Response> - - @GET("v3/settings/history/{from}") - suspend fun getSettingsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Int = 100): Response>> - - @GET("v3/settings") - suspend fun searchSettings(@Query("limit") limit: Int = 100): Response>> - - @POST("v3/settings") - suspend fun createSetting(@Body settings: JsonObject): Response - - @PATCH("v3/settings/{identifier}") - suspend fun patchSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response - - /** PUT (NS3 "UPDATE") = upsert. Replaces existing doc, or inserts if absent. */ - @PUT("v3/settings/{identifier}") - suspend fun updateSetting(@Body settings: JsonObject, @Path("identifier") identifier: String): Response - - /** [permanent] = `null` → soft delete (tombstone); `true` → NS `?permanent=true` hard delete. */ - @DELETE("v3/settings/{identifier}") - suspend fun deleteSetting(@Path("identifier") identifier: String, @Query("permanent") permanent: Boolean?): Response - -} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt new file mode 100644 index 000000000000..3aaa431cc89d --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt @@ -0,0 +1,106 @@ +package app.aaps.core.nssdk.networking + +import app.aaps.core.nssdk.exceptions.DateHeaderOutOfToleranceException +import app.aaps.core.nssdk.exceptions.InvalidAccessTokenException +import app.aaps.core.nssdk.networking.Status.MESSAGE_DATE_HEADER_OUT_OF_TOLERANCE +import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteAuthResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.save +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.URLBuilder +import io.ktor.http.appendPathSegments +import io.ktor.http.takeFrom + +/** + * The Nightscout token dance, as an `HttpSend` interceptor. + * + * This is written by hand rather than with Ktor's `Auth` / `bearer` provider, because that provider + * differs in ways that lose data without saying so: + * + * - it **omits** the header when it has no token. Nightscout does not reject an unauthenticated + * request - it answers **200 with the anonymous role**, which flows into `hasWritePermission` and + * makes `DataSyncWorker` skip the whole upload block. Nothing is logged. Here the header is always + * sent, even while the token is still empty. + * - it refreshes on 401 only; Nightscout also answers **403**. + * - its refresh hook never sees the response body, so the clock-skew case below could not exist. + * + * `NsSdkAuthContractTest` pins all of it. + */ +internal object NsAuth { + + fun install(client: HttpClient, refreshClient: HttpClient, baseUrl: String, refreshToken: String) { + // The current Bearer token. Deliberately unsynchronised, matching the Retrofit interceptor: + // concurrent 401s each refresh. Serialising them is an improvement, but one to make on + // purpose with its own test rather than as a side effect of changing transport. + var jwtToken = "" + + client.plugin(HttpSend).intercept { request -> + request.headers[HttpHeaders.Date] = nowMillis().toString() + request.headers[HttpHeaders.Authorization] = "Bearer $jwtToken" + + // save() so the body can be read here AND again by the caller - Ktor streams it once. + val firstCall = execute(request).save() + if (firstCall.response.status.value !in REFRESHABLE) return@intercept firstCall + + // Checked BEFORE refreshing: a clock that is too far off is not fixed by a new token, and + // the caller needs to see this specific error to tell the user to fix the time. + if (firstCall.response.bodyAsText().contains(MESSAGE_DATE_HEADER_OUT_OF_TOLERANCE)) + throw DateHeaderOutOfToleranceException("Data header out of tolerance") + + val refreshed = refresh(refreshClient, baseUrl, refreshToken) ?: return@intercept firstCall + jwtToken = refreshed + + // Rebuilt rather than replayed, so Date is recomputed along with the new token. Exactly + // one retry - never a loop. + request.headers[HttpHeaders.Date] = nowMillis().toString() + request.headers[HttpHeaders.Authorization] = "Bearer $jwtToken" + execute(request) + } + } + + /** + * Asks for a new access token. + * + * Returns the token on success, or null to mean "carry on with the original failed response" - + * which is what the Retrofit version did for any refresh answer that was neither 200 nor a + * 401/403. + */ + private suspend fun refresh(refreshClient: HttpClient, baseUrl: String, refreshToken: String): String? { + val response = refreshClient.get(refreshUrl(baseUrl, refreshToken)) + return when { + response.status.value in REFRESHABLE -> + throw InvalidAccessTokenException("Invalid access token") + + response.status.value != 200 -> null + else -> + runCatching { + nsSdkJson.decodeFromString(RemoteAuthResponse.serializer(), response.bodyAsText()).token + }.getOrNull() + } + } + + /** + * `https:///api/v2/authorization/request/`, built from the **host only**. + * + * The Retrofit annotation began with a slash (`/api/v2/...`), which meant it was resolved against + * the host root and any sub-path was dropped. That is reproduced here on purpose: whether a + * sub-path install should keep its sub-path when refreshing is a Nightscout question, and this + * change is about transport, not about answering it. + */ + private fun refreshUrl(baseUrl: String, refreshToken: String): String = + URLBuilder().apply { + takeFrom(baseUrl) + encodedPathSegments = emptyList() + appendPathSegments(listOf("api", "v2", "authorization", "request", refreshToken), encodeSlash = true) + }.buildString() + + private fun nowMillis(): Long = System.currentTimeMillis() + + /** Nightscout answers 403 as well as 401 for an expired token. */ + private val REFRESHABLE = listOf(401, 403) +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt new file mode 100644 index 000000000000..65baa159ba2d --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt @@ -0,0 +1,59 @@ +package app.aaps.core.nssdk.networking + +import app.aaps.core.nssdk.nsSdkJson +import kotlinx.serialization.DeserializationStrategy + +/** + * One HTTP response, with the body already read into a string. + * + * Reading the body **once, up front** is the whole point of this type. Retrofit handed out a + * `Response` with a separate `body()` and `errorBody()`, and the client leans on that: an error + * body is inspected as raw text to decide whether to re-send a record with `utcOffset = 0` + * (`NSAndroidClientImpl.createSgv`). Ktor has no such split - the body is a stream that can be + * consumed only once - so a port that reads it lazily, or reads it only on success, silently loses + * that text and drops the record for good. + * + * Holding the text also means the status ladders in `NSAndroidClientImpl` can stay as they are. + * + * Bodies here are small: single documents, or pages already limited by a `limit` parameter. + */ +internal class NsHttpResponse( + val code: Int, + private val eTagHeader: String?, + /** Raw response text, always read, success or not. */ + val bodyText: String, + private val deserializer: DeserializationStrategy +) { + + val isSuccessful: Boolean get() = code in 200..299 + + /** + * The decoded body, or null if it cannot be decoded. + * + * Null rather than a throw, because that is what Retrofit's `body()` did for an error response, + * and several call sites read `response.body()?.identifier` on paths that also accept failure. + */ + fun body(): T? = + if (bodyText.isEmpty()) null + else runCatching { nsSdkJson.decodeFromString(deserializer, bodyText) }.getOrNull() + + /** + * The raw text of a failed response, matching Retrofit's `errorBody()?.string()`. + * + * Null for a successful response so the callers' `errorResponse ?: response.message()` fallbacks + * behave as before. + */ + fun errorBody(): String? = if (isSuccessful) null else bodyText.ifEmpty { null } + + /** Stand-in for Retrofit's `Response.message()`, used only as a fallback when there is no body. */ + fun message(): String = "HTTP $code" + + /** + * `lastServerModified`, parsed from the weak ETag Nightscout sends (`W/""`). + * + * The arithmetic is copied exactly from the Retrofit version, including the fact that it is + * positional rather than a pattern match, so a header in any other shape still throws. Real + * Nightscout always sends the weak form; `NsSdkResponseContractTest` pins both cases. + */ + fun eTagAsLong(): Long? = eTagHeader?.substring(3, eTagHeader.length - 1)?.toLong() +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt new file mode 100644 index 000000000000..9a6f1372d4e1 --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt @@ -0,0 +1,90 @@ +package app.aaps.core.nssdk.networking + +import app.aaps.core.nssdk.nsSdkJson +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json + +/** + * Builds the Ktor client used to talk to Nightscout. + * + * The engine is **OkHttp**, so this reuses the HTTP stack the app already ships rather than adding a + * second one. When the module builds for iOS the engine becomes Darwin and nothing else here + * changes - that is the reason for moving off Retrofit at all. + * + * Two settings are load bearing and must not be "improved": + * + * - **`expectSuccess = false`.** Ktor's default throws on any non-2xx. The whole client is written + * the other way round: a 4xx is a value, not an exception. `getSettings` returns a 404 result, + * `updateSvg` and `deleteSettings` treat 404 as success, and every write returns + * `CreateUpdateResponse(response = )`. Turning this on would make all of that dead code - + * and because `ClientRequestException` is not in the retry exclusion list, each of those calls + * would also be retried four times before failing. See `NsSdkStatusContractTest`. + * - **No `HttpRequestRetry` plugin.** Retries are already done one level up by + * `NSAndroidClientImpl.callWrapper`. Adding Ktor's plugin would multiply with it, and the + * `utcOffset` fallback re-enters a public method, so a single reading could produce well over ten + * POSTs inside one sync. + * + * There is deliberately **no cache**. The OkHttp disk cache existed only so a revalidated GET could + * surface as a 304, which the paging workers used as their stop condition. That signal is replaced + * by "stop when the cursor cannot advance", which says what it means and needs no `Context`. + */ +internal object NsKtorClient { + + /** + * Everything the client needs, built together so the two [HttpClient]s can be closed as a pair. + * + * There are two on purpose: the refresh call must not carry an `Authorization` header, or the + * interceptor would recurse into itself. + */ + class Stack(val api: NightscoutApi, private val main: HttpClient, private val refresh: HttpClient) { + + fun close() { + main.close() + refresh.close() + } + } + + fun stack(baseUrl: String, refreshToken: String, logging: Boolean, logger: (String) -> Unit): Stack { + val main = build(logging, logger) + val refresh = build(logging, logger) + NsAuth.install(main, refresh, baseUrl, refreshToken) + return Stack(NightscoutApi(main, baseUrl), main, refresh) + } + + fun build(logging: Boolean, logger: (String) -> Unit): HttpClient = HttpClient(OkHttp) { + expectSuccess = false + + install(ContentNegotiation) { + json(nsSdkJson) + } + + install(HttpTimeout) { + socketTimeoutMillis = SOCKET_TIMEOUT + connectTimeoutMillis = CONNECT_TIMEOUT + } + + if (logging) { + engine { + addInterceptor { chain -> + val request = chain.request() + logger("--> ${request.method} ${request.url}") + val response = chain.proceed(request) + logger("<-- ${response.code} ${request.url}") + response + } + } + } + } + + /** Matches the old OkHttp read timeout. */ + private const val SOCKET_TIMEOUT = 60L * 1000 + + /** + * New, and deliberately so: OkHttp applied its own 10 s connect default, which Ktor does not. + * Leaving it unset would let a dead host hang until the socket timeout instead. + */ + private const val CONNECT_TIMEOUT = 10L * 1000 +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt new file mode 100644 index 000000000000..7e12e5345c4a --- /dev/null +++ b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt @@ -0,0 +1,26 @@ +package app.aaps.core.nssdk.networking + +/** + * Turns what the caller supplies into the base URL the client uses. + * + * This is all that is left of the old `NetworkStackBuilder`: the OkHttp client, its disk cache, the + * two Retrofit instances and the auth interceptor are gone, replaced by [NsKtorClient] and + * [NsAuth]. + */ +internal object NsUrl { + + /** + * Production callers pass a bare host with an optional sub-path - `NSClientV3Plugin.setClient` + * strips the scheme first - so `host.com` and `host.com/ns` behave exactly as before. + * + * An input that already carries a scheme is used as it stands. That is what lets a unit test + * point the client at `http://localhost:`, and it also stops a stored `http://host` from + * turning into `https://http://host/api/`, which is what the old string concatenation produced + * (the caller only strips `https://`). + */ + fun toBaseUrl(hostOrUrl: String): String { + val trimmed = hostOrUrl.trimEnd('/') + val withScheme = if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) trimmed else "https://$trimmed" + return "$withScheme/api/" + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt new file mode 100644 index 000000000000..1949df2eea16 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt @@ -0,0 +1,69 @@ +package app.aaps.core.nssdk.exceptions + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +/** + * Asserts that every exception this client throws is still a `java.io.IOException` on the JVM. + * + * The base class moved from `java.io.IOException` to `kotlinx.io.IOException` so the module can build + * for iOS. On the JVM kotlinx-io declares + * `actual typealias IOException = java.io.IOException`, so nothing changes - but "so it should be + * fine" is exactly the reasoning that let the Gson regression through, and the failure mode here is + * ugly: a `catch (e: IOException)` that silently stops catching. The compiler would not complain, + * and neither would any other test. + * + * Three call sites depend on it: + * - `PairingOfferFetcher.fetchAndUnwrap` - a network failure while looking for pairing offers must + * surface as "no offer", not as a crash on the pairing screen. + * - `PairingOfferPublisher.publishOffer` / `deleteOffer` - `deleteOffer` must never throw, because a + * pairing offer left behind on the server keeps a PIN brute-force window open. + * + * If this file ever fails, do not change the assertion - the exception hierarchy has silently + * stopped being catchable and those three sites need looking at. + */ +class NightscoutExceptionTest { + + private val all = listOf( + DateHeaderOutOfToleranceException("m"), + InvalidAccessTokenException("m"), + InvalidFormatNightscoutException("m"), + InvalidParameterNightscoutException("m"), + UnknownResponseNightscoutException("m"), + UnsuccessfulNightscoutException("m") + ) + + @Test + fun `every exception is a java IOException`() { + for (exception in all) + assertThat(exception).isInstanceOf(java.io.IOException::class.java) + } + + /** The catch sites are written against `java.io.IOException`, so prove that shape works. */ + @Test + fun `each one is caught by a java IOException catch block`() { + for (exception in all) { + var caught = false + try { + throw exception + } catch (_: java.io.IOException) { + caught = true + } + assertThat(caught).isTrue() + } + } + + /** They all keep a non-null message - workers write it straight into the user visible log. */ + @Test + fun `every exception carries its message`() { + for (exception in all) + assertThat(exception.message).isEqualTo("m") + } + + /** And they are all still `NightscoutException`, which is what the retry exclusion list matches on. */ + @Test + fun `every exception is a NightscoutException`() { + for (exception in all) + assertThat(exception).isInstanceOf(NightscoutException::class.java) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt new file mode 100644 index 000000000000..d789aad13557 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt @@ -0,0 +1,154 @@ +package app.aaps.core.nssdk.networking + +import app.aaps.core.nssdk.remotemodel.RemoteFood +import com.google.common.truth.Truth.assertThat +import io.ktor.client.HttpClient +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +/** + * The same URL expectations as `NsSdkUrlContractTest`, asserted against the **Ktor** endpoint class + * directly, before it is wired into `NSAndroidClientImpl`. + * + * Running it separately means the riskiest single question - does Ktor keep the `$` in `date$gt`, or + * turn it into `%24`? - is answered before the swap rather than during it. A percent-encoded operator + * is not an error to Nightscout: it is an unknown filter, which is ignored, and the server answers + * **200 with unfiltered rows**. + * + * Every expected string here is copied from the Retrofit contract test, which is green against the + * live stack. When both files agree, the URL half of the port is done. + */ +class NightscoutApiUrlTest { + + private lateinit var server: MockWebServer + private lateinit var httpClient: HttpClient + private lateinit var api: NightscoutApi + + @BeforeEach + fun setUp() { + server = MockWebServer() + server.start() + httpClient = NsKtorClient.build(logging = false, logger = { }) + api = NightscoutApi(httpClient, NsUrl.toBaseUrl(server.url("/").toString().trimEnd('/'))) + } + + @AfterEach + fun tearDown() { + httpClient.close() + server.close() + } + + private suspend fun pathOf(body: String = """{"result":[]}""", call: suspend () -> Unit): String { + server.enqueue(MockResponse.Builder().code(200).body(body).build()) + runCatching { call() } + return server.takeRequest().target + } + + // ---------------------------------------------------------------- the $ operators + + @Test + fun `entries keep their dollar operators and static parts`() = runTest { + assertThat(pathOf { api.getSgvs() }) + .isEqualTo("/api/v3/entries?sort\$desc=date&type=sgv") + + assertThat(pathOf { api.getSgvsNewerThan(1700000000000, 250) }) + .isEqualTo("/api/v3/entries?sort=date&date\$gt=1700000000000&limit=250") + + assertThat(pathOf { api.getSgvsModifiedSince(1700000000000, 250) }) + .isEqualTo("/api/v3/entries/history/1700000000000?limit=250") + } + + @Test + fun `treatments keep their dollar operators and static parts`() = runTest { + assertThat(pathOf { api.getTreatmentsNewerThan("2024-01-01T00:00:00.000Z", 250) }) + .isEqualTo("/api/v3/treatments?sort=created_at&created_at\$gt=2024-01-01T00:00:00.000Z&limit=250") + + assertThat(pathOf { api.getTreatmentsModifiedSince(1700000000000, 250) }) + .isEqualTo("/api/v3/treatments/history/1700000000000?limit=250") + } + + @Test + fun `getLastProfile keeps sort and limit`() = runTest { + assertThat(pathOf { api.getLastProfile() }) + .isEqualTo("/api/v3/profile?sort\$desc=date&limit=1") + } + + @Test + fun `getProfileModifiedSince keeps the default limit`() = runTest { + assertThat(pathOf { api.getProfileModifiedSince(1700000000000) }) + .isEqualTo("/api/v3/profile/history/1700000000000?limit=10") + } + + // ---------------------------------------------------------------- settings + + @Test + fun `settings endpoints`() = runTest { + assertThat(pathOf(body = """{"result":{}}""") { api.getSetting("aaps") }) + .isEqualTo("/api/v3/settings/aaps") + + assertThat(pathOf { api.getSettingsModifiedSince(1700000000000, 100) }) + .isEqualTo("/api/v3/settings/history/1700000000000?limit=100") + + assertThat(pathOf { api.searchSettings(100) }) + .isEqualTo("/api/v3/settings?limit=100") + } + + /** Absence of `permanent` is what makes the delete a soft delete. */ + @Test + fun `a soft delete sends no permanent parameter`() = runTest { + val path = pathOf(body = """{}""") { api.deleteSetting("aaps_x", null) } + + assertThat(path).isEqualTo("/api/v3/settings/aaps_x") + assertThat(path).doesNotContain("permanent") + } + + @Test + fun `a permanent delete sends permanent true`() = runTest { + assertThat(pathOf(body = """{}""") { api.deleteSetting("aaps_x", true) }) + .isEqualTo("/api/v3/settings/aaps_x?permanent=true") + } + + // ---------------------------------------------------------------- writes + + @Test + fun `write endpoints target the right paths`() = runTest { + assertThat(pathOf(body = """{}""") { api.createSetting(JsonObject(emptyMap())) }) + .isEqualTo("/api/v3/settings") + + assertThat(pathOf(body = """{}""") { api.updateSetting(JsonObject(emptyMap()), "aaps") }) + .isEqualTo("/api/v3/settings/aaps") + + assertThat(pathOf(body = """{}""") { api.createProfile(JsonObject(emptyMap())) }) + .isEqualTo("/api/v3/profile") + } + + /** An identifier with characters that would otherwise split the path must stay one segment. */ + @Test + fun `an identifier is percent encoded into a single path segment`() = runTest { + val path = pathOf(body = """{"result":{}}""") { api.getSetting("a/b c") } + + assertThat(path).startsWith("/api/v3/settings/") + assertThat(path.removePrefix("/api/v3/settings/")).isEqualTo("a%2Fb%20c") + } + + // ---------------------------------------------------------------- food is deliberately inert + + /** + * Food update and delete must not reach the network. See `NightscoutApi.updateFood` - sending + * these would be a collection-wide request against a live Nightscout. + */ + @Test + fun `food update and delete send nothing`() = runTest { + val food = RemoteFood(name = "Apple", portion = 100.0, carbs = 12, identifier = "x", isValid = true, isReadOnly = false) + + runCatching { api.updateFood(food, identifier = "x") } + runCatching { api.deleteFood("x") } + + assertThat(server.requestCount).isEqualTo(0) + } +} diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt index 75dd91f95a49..22849692467c 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk.networking -import android.content.Context import app.aaps.core.nssdk.NSAndroidClientImpl import app.aaps.core.nssdk.exceptions.DateHeaderOutOfToleranceException import app.aaps.core.nssdk.exceptions.InvalidAccessTokenException @@ -15,10 +14,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.io.File -import java.nio.file.Files import java.util.concurrent.atomic.AtomicInteger /** @@ -44,7 +39,6 @@ class NsSdkAuthContractTest { private lateinit var server: MockWebServer private lateinit var client: NSAndroidClientImpl - private lateinit var cacheDir: File private val refreshCalls = AtomicInteger(0) private var lastRefreshTarget: String? = null @@ -52,14 +46,11 @@ class NsSdkAuthContractTest { @BeforeEach fun setUp() { - cacheDir = Files.createTempDirectory("nssdk-auth-cache").toFile() server = MockWebServer() server.start() - val context = mock { on { getCacheDir() } doReturn cacheDir } client = NSAndroidClientImpl( baseUrl = server.url("/").toString().trimEnd('/'), accessToken = "REFRESH_TOKEN", - context = context, logging = false, logger = { }, dispatcher = Dispatchers.Unconfined @@ -69,7 +60,6 @@ class NsSdkAuthContractTest { @AfterEach fun tearDown() { server.close() - cacheDir.deleteRecursively() } /** diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt index 3d63e2ad02a4..5d6a24b58a27 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk.networking -import android.content.Context import app.aaps.core.nssdk.NSAndroidClientImpl import app.aaps.core.nssdk.localmodel.entry.Direction import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 @@ -16,10 +15,6 @@ import mockwebserver3.MockWebServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.io.File -import java.nio.file.Files /** * Pins the behaviour that depends on reading the **raw text of an error body**. @@ -42,18 +37,14 @@ class NsSdkErrorBodyContractTest { private lateinit var server: MockWebServer private lateinit var client: NSAndroidClientImpl - private lateinit var cacheDir: File @BeforeEach fun setUp() { - cacheDir = Files.createTempDirectory("nssdk-errorbody-cache").toFile() server = MockWebServer() server.start() - val context = mock { on { getCacheDir() } doReturn cacheDir } client = NSAndroidClientImpl( baseUrl = server.url("/").toString().trimEnd('/'), accessToken = "token", - context = context, logging = false, logger = { }, dispatcher = Dispatchers.Unconfined @@ -63,7 +54,6 @@ class NsSdkErrorBodyContractTest { @AfterEach fun tearDown() { server.close() - cacheDir.deleteRecursively() } private fun sgv(utcOffset: Long) = NSSgvV3( diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt index 59730af13318..782fa6c862bc 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk.networking -import android.content.Context import app.aaps.core.nssdk.NSAndroidClientImpl import app.aaps.core.nssdk.exceptions.InvalidFormatNightscoutException import app.aaps.core.nssdk.localmodel.entry.Direction @@ -17,10 +16,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.io.File -import java.nio.file.Files /** * Pins how a response is turned into values: the **ETag**, which drives the sync cursor, and the @@ -40,18 +35,14 @@ class NsSdkResponseContractTest { private lateinit var server: MockWebServer private lateinit var client: NSAndroidClientImpl - private lateinit var cacheDir: File @BeforeEach fun setUp() { - cacheDir = Files.createTempDirectory("nssdk-response-cache").toFile() server = MockWebServer() server.start() - val context = mock { on { getCacheDir() } doReturn cacheDir } client = NSAndroidClientImpl( baseUrl = server.url("/").toString().trimEnd('/'), accessToken = "token", - context = context, logging = false, logger = { }, dispatcher = Dispatchers.Unconfined @@ -61,7 +52,6 @@ class NsSdkResponseContractTest { @AfterEach fun tearDown() { server.close() - cacheDir.deleteRecursively() } private fun sgv() = NSSgvV3( diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt index 0e775b9a3d1f..06a3dbaaef9b 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk.networking -import android.content.Context import app.aaps.core.nssdk.NSAndroidClientImpl import app.aaps.core.nssdk.exceptions.InvalidParameterNightscoutException import app.aaps.core.nssdk.exceptions.UnsuccessfulNightscoutException @@ -15,10 +14,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.io.File -import java.nio.file.Files /** * Pins how each HTTP status is turned into a result, and **how many requests that costs**. @@ -46,18 +41,14 @@ class NsSdkStatusContractTest { private lateinit var server: MockWebServer private lateinit var client: NSAndroidClientImpl - private lateinit var cacheDir: File @BeforeEach fun setUp() { - cacheDir = Files.createTempDirectory("nssdk-status-cache").toFile() server = MockWebServer() server.start() - val context = mock { on { getCacheDir() } doReturn cacheDir } client = NSAndroidClientImpl( baseUrl = server.url("/").toString().trimEnd('/'), accessToken = "token", - context = context, logging = false, logger = { }, dispatcher = Dispatchers.Unconfined @@ -67,7 +58,6 @@ class NsSdkStatusContractTest { @AfterEach fun tearDown() { server.close() - cacheDir.deleteRecursively() } /** Answer every request with the same status and body - enough for the retry cases. */ @@ -104,6 +94,25 @@ class NsSdkStatusContractTest { assertThat(server.requestCount).isEqualTo(1) } + /** + * A bare 304 is an error, and always was. + * + * Nightscout only answers 304 to a **conditional** request. OkHttp sent those because it had a + * disk cache entry with an ETag, and it then merged the 304 with the cached body and handed + * Retrofit a **200** whose `raw().networkResponse.code` was 304 - which is the only way + * `ReadResponse.code` could ever hold 304, and what the paging workers used as their stop signal. + * + * There is no cache now, so no conditional request is sent and no 304 comes back. If one ever did + * arrive it would fail `isSuccessful` (200..299) and throw, exactly as it would have before when + * there was no cached entry to merge with. Pinned so that stays true. + */ + @Test + fun `a bare 304 throws, as it did without a cache entry`() = runTest { + alwaysRespond(304) + + assertThrows { client.getSgvsModifiedSince(1700000000000, 100) } + } + // ---------------------------------------------------------------- 404 as a normal answer /** `getSettings` turns 404 into an empty result, because "no settings doc yet" is not an error. */ diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt index 0000d197b4c4..a7a9b093190b 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt @@ -1,6 +1,5 @@ package app.aaps.core.nssdk.networking -import android.content.Context import app.aaps.core.nssdk.NSAndroidClientImpl import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -10,10 +9,6 @@ import mockwebserver3.MockWebServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import java.io.File -import java.nio.file.Files /** * Pins the exact URL every endpoint puts on the wire. @@ -42,7 +37,6 @@ class NsSdkUrlContractTest { private lateinit var server: MockWebServer private lateinit var client: NSAndroidClientImpl - private lateinit var cacheDir: File /** * A plain temp directory, not JUnit's `@TempDir`. @@ -54,15 +48,12 @@ class NsSdkUrlContractTest { */ @BeforeEach fun setUp() { - cacheDir = Files.createTempDirectory("nssdk-test-cache").toFile() server = MockWebServer() server.start() - val context = mock { on { getCacheDir() } doReturn cacheDir } client = NSAndroidClientImpl( // Carries a scheme, so it is used as it stands and points at the local server. baseUrl = server.url("/").toString().trimEnd('/'), accessToken = "token", - context = context, logging = false, logger = { }, dispatcher = Dispatchers.Unconfined @@ -72,7 +63,6 @@ class NsSdkUrlContractTest { @AfterEach fun tearDown() { server.close() - cacheDir.deleteRecursively() // best effort, see setUp } /** Runs [call], ignores how it ends, and returns the request line the server saw. */ @@ -164,20 +154,33 @@ class NsSdkUrlContractTest { .isEqualTo("/api/v3/settings/aaps_x?permanent=true") } + /** + * How a path parameter is encoded. Identifiers come from Nightscout and are normally ids with no + * special characters, so this is a latent edge rather than a live case - but it decides whether a + * odd identifier addresses one document or a different path entirely, so the port has to match it. + */ + @Test + fun `an identifier is encoded into the path`() = runTest { + val path = pathOf(body = """{"result":{}}""") { client.getSettings("a/b c") } + + assertThat(path).startsWith("/api/v3/settings/") + assertThat(path.removePrefix("/api/v3/settings/")).isEqualTo("a%2Fb%20c") + } + // ---------------------------------------------------------------- base url shapes /** A sub-path install must keep its sub-path on the data endpoints. */ @Test fun `a sub-path install keeps the sub-path`() { - assertThat(NetworkStackBuilder.toBaseUrl("host.com/ns")).isEqualTo("https://host.com/ns/api/") - assertThat(NetworkStackBuilder.toBaseUrl("host.com")).isEqualTo("https://host.com/api/") - assertThat(NetworkStackBuilder.toBaseUrl("host.com/")).isEqualTo("https://host.com/api/") + assertThat(NsUrl.toBaseUrl("host.com/ns")).isEqualTo("https://host.com/ns/api/") + assertThat(NsUrl.toBaseUrl("host.com")).isEqualTo("https://host.com/api/") + assertThat(NsUrl.toBaseUrl("host.com/")).isEqualTo("https://host.com/api/") } /** An explicit scheme is honoured - this is what lets the tests above reach localhost. */ @Test fun `an explicit scheme is kept`() { - assertThat(NetworkStackBuilder.toBaseUrl("http://localhost:8080")).isEqualTo("http://localhost:8080/api/") - assertThat(NetworkStackBuilder.toBaseUrl("https://host.com")).isEqualTo("https://host.com/api/") + assertThat(NsUrl.toBaseUrl("http://localhost:8080")).isEqualTo("http://localhost:8080/api/") + assertThat(NsUrl.toBaseUrl("https://host.com")).isEqualTo("https://host.com/api/") } } diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt new file mode 100644 index 000000000000..4c0e49265f76 --- /dev/null +++ b/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt @@ -0,0 +1,143 @@ +package app.aaps.core.nssdk.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +/** + * Golden vectors for the client-control crypto. + * + * These are fixed inputs with their exact expected outputs, produced by the current JVM + * implementation. They exist so a **second** implementation - Kotlin/Native for iOS, a desktop + * target, or a different library - can be checked byte for byte rather than by "it is also + * HMAC-SHA256". + * + * That distinction matters because the wire format is already frozen: AAPS masters and clients in + * the field exchange these values today. The primitives themselves are standards and interoperate by + * definition, but the **packaging** is where implementations differ and where a mismatch is silent: + * + * - **GCM tag placement.** JCE returns `ciphertext ‖ tag` from a single `doFinal`. Apple's CryptoKit + * models a sealed box with the tag held separately. Concatenate them differently and every unwrap + * fails - which this code deliberately reports as "wrong PIN", so it would look like user error. + * - **Hex case.** Signatures are compared as text with a constant-time string compare. One side + * emitting uppercase means every signature check fails. + * - **PIN encoding.** `PBEKeySpec` takes a char array; the PIN is ASCII digits, so any sane encoding + * agrees - but a non-ASCII PIN would not, which is worth knowing before anyone widens the alphabet. + * + * If a value here ever needs changing, the wire format has changed and every deployed AAPS stops + * being able to pair or verify. Treat a failure as a real incompatibility, not a stale test. + */ +class ClientControlCryptoVectorsTest { + + private fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02x".format(it) } + private fun bytes(vararg values: Int) = ByteArray(values.size) { values[it].toByte() } + + /** 32 bytes, 0x00..0x1f - the HMAC secret length the protocol uses. */ + private val secret = ByteArray(32) { it.toByte() } + + /** 16 bytes, 0xa0.. - a fixed stand-in for a generated salt. */ + private val salt = ByteArray(16) { (0xa0 + it).toByte() } + + /** 12 bytes, 0xb0.. - a fixed stand-in for a generated IV. */ + private val iv = ByteArray(12) { (0xb0 + it).toByte() } + + // ================================================================ HMAC-SHA256 + + /** + * The signature over a canonical envelope string. Every command and every ack rides on this. + */ + @Test + fun `hmac vector - canonical command string`() { + val canonical = "clientId=abc|counter=5|type=SceneStop|timestamp=1785992179555" + + val signature = ClientControlCrypto.sign(secret, canonical) + + assertThat(signature).isEqualTo("e2ac7455db1ee06145fe8432e115eaca1a4e81375703ad907251cbb9d223d216") + } + + /** An empty payload, to pin the degenerate case as well. */ + @Test + fun `hmac vector - empty string`() { + assertThat(ClientControlCrypto.sign(secret, "")).isEqualTo("d38b42096d80f45f826b44a9d5607de72496a415d3f4a1a8c88e3bb9da8dc1cb") + } + + /** Non-ASCII input, so the UTF-8 encoding of the signed text is pinned too. */ + @Test + fun `hmac vector - non ascii`() { + assertThat(ClientControlCrypto.sign(secret, "poznámka ěščřž")).isEqualTo("fcefc8439c7d9f1d149bee6a4dfa2ec4631f8d23a8fd35bc9444c86a4a19b36d") + } + + /** The signature is lower-case hex, 64 characters. Compared as text, so case is part of the format. */ + @Test + fun `signatures are lower case hex of 64 characters`() { + val signature = ClientControlCrypto.sign(secret, "anything") + + assertThat(signature).hasLength(64) + assertThat(signature).isEqualTo(signature.lowercase()) + assertThat(signature.all { it.isDigit() || it in 'a'..'f' }).isTrue() + } + + // ================================================================ PBKDF2 + AES-256-GCM + + /** + * The wrapped pairing payload: PBKDF2-HMAC-SHA256 (200 000 iterations, 256 bit) over the PIN and + * salt, then AES-256-GCM with a 12 byte IV and a 128 bit tag. + */ + @Test + fun `wrap vector - pairing payload`() { + val plaintext = """{"clientId":"abc","secret":"00","expiresAt":1785992179555}""".toByteArray(Charsets.UTF_8) + + val wrapped = ClientControlPairingCrypto.wrap(plaintext, pin = "12345678", salt = salt, iv = iv) + + assertThat(hex(wrapped)).isEqualTo("f864c764382a5e82af17fccf545a14fea3b29eae0784eb2b1630ffe179e58b31c5fc8291afde74f7a0867579b244c6213a12eb9bf1382b1b6c569d2d1f74c25b52c6b253943d87026018") + } + + /** A short, fixed plaintext - easier to eyeball, and pins the tag placement on its own. */ + @Test + fun `wrap vector - short plaintext`() { + val wrapped = ClientControlPairingCrypto.wrap(bytes(1, 2, 3, 4), pin = "00000000", salt = salt, iv = iv) + + assertThat(hex(wrapped)).isEqualTo("f0ef78af68d41f05d7bfed02b15a252856f3620c") + } + + /** + * The GCM tag is **appended** to the ciphertext, so the output is 16 bytes longer than the input. + * An implementation that keeps the tag separate would produce a shorter blob and fail to unwrap. + */ + @Test + fun `the gcm tag is appended, making the output 16 bytes longer`() { + val plaintext = ByteArray(40) { it.toByte() } + + val wrapped = ClientControlPairingCrypto.wrap(plaintext, pin = "12345678", salt = salt, iv = iv) + + assertThat(wrapped.size).isEqualTo(plaintext.size + 16) + } + + // ================================================================ round trip + + @Test + fun `wrap and unwrap round trip`() { + val plaintext = "hello pairing".toByteArray(Charsets.UTF_8) + + val wrapped = ClientControlPairingCrypto.wrap(plaintext, pin = "13572468", salt = salt, iv = iv) + val unwrapped = ClientControlPairingCrypto.unwrap(wrapped, pin = "13572468", salt = salt, iv = iv) + + assertThat(unwrapped).isEqualTo(plaintext) + } + + /** A wrong PIN gives null, not an exception - callers cannot tell it from a corrupt blob. */ + @Test + fun `a wrong pin gives null`() { + val wrapped = ClientControlPairingCrypto.wrap("x".toByteArray(), pin = "11111111", salt = salt, iv = iv) + + assertThat(ClientControlPairingCrypto.unwrap(wrapped, pin = "22222222", salt = salt, iv = iv)).isNull() + } + + /** Tampering with a single byte must fail the auth tag. */ + @Test + fun `a tampered blob gives null`() { + val wrapped = ClientControlPairingCrypto.wrap("x".toByteArray(), pin = "11111111", salt = salt, iv = iv) + wrapped[0] = (wrapped[0] + 1).toByte() + + assertThat(ClientControlPairingCrypto.unwrap(wrapped, pin = "11111111", salt = salt, iv = iv)).isNull() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5722e7dd63b0..486eb5c20be3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ junit = "6.1.2" robolectric = "4.16.1" okhttp = "5.4.0" retrofit = "3.0.0" +ktor = "3.3.0" kotlinx-serialization = "1.11.0" kotlinx-coroutines = "1.11.0" work = "2.11.2" @@ -130,6 +131,11 @@ com-squareup-leakcanary-android = { group = "com.squareup.leakcanary", name = "l com-squareup-okhttp3-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } com-squareup-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } com-squareup-okhttp3-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver3-junit5", version.ref = "okhttp" } + +io-ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +io-ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } +io-ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +io-ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } com-squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } com-squareup-retrofit2-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 37de0da3e95b..910d8c53b105 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -327,6 +327,9 @@ class NSClientV3Plugin @Inject constructor( } val restartOnChange: suspend (Any) -> Unit = { stopService() + // Release the HTTP engine before dropping the reference. The Retrofit client leaked + // quietly here; a Ktor engine holds real connections, so it is closed explicitly. + nsAndroidClient?.close() nsAndroidClient = null setClient() nsClientRepository.updateUrl(preferences.get(StringKey.NsClientUrl)) @@ -618,7 +621,6 @@ class NSClientV3Plugin @Inject constructor( nsAndroidClient = NSAndroidClientImpl( baseUrl = preferences.get(StringKey.NsClientUrl).lowercase().replace("https://", "").replace(Regex("/$"), ""), accessToken = preferences.get(StringKey.NsClientAccessToken), - context = context, logging = l.findByName(LTag.NSCLIENT.tag).enabled && (config.isEngineeringMode() || config.isDev()), logger = { msg -> aapsLogger.debug(LTag.HTTP, msg) } ) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt index 786d95de4039..2b7c8c2b636a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorker.kt @@ -54,10 +54,21 @@ class LoadBgWorker @AssistedInject constructor( if ((nsClientV3Plugin.newestDataOnServer?.collections?.entries ?: Long.MAX_VALUE) > lastLoaded) { val sgvs: List val response: NSAndroidClient.ReadResponse>? + // Replaces the old `response.code != 304` brake. + // + // A 304 could only ever appear because OkHttp's disk cache revalidated a GET, and + // that cache is gone with Retrofit. What the 304 actually meant is tested directly + // now: the server has nothing newer, so the cursor cannot move and another page + // would return the same rows for ever. + // + // Only the modified-since path can stall like this. The first load pages by date + // and always moves forward, and its response carries no server timestamp at all. + var cursorStalled = false if (isFirstLoad) response = nsAndroidClient.getSgvsNewerThan(lastLoaded, NSClientV3Plugin.RECORDS_TO_LOAD) else { response = nsAndroidClient.getSgvsModifiedSince(lastLoaded, NSClientV3Plugin.RECORDS_TO_LOAD) aapsLogger.debug(LTag.NSCLIENT, "lastLoadedSrvModified: ${response.lastServerModified}") + cursorStalled = response.lastServerModified?.let { it <= lastLoaded } == true response.lastServerModified?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.entries = it } nsClientV3Plugin.storeLastLoadedSrvModified() nsClientV3Plugin.scheduleIrregularExecution() // Idea is to run after 5 min after last BG @@ -74,7 +85,7 @@ class LoadBgWorker @AssistedInject constructor( val action = if (isFirstLoad) "RCV-F" else "RCV" nsClientRepository.addLog("◄ $action", "${sgvs.size} SVGs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}") // Schedule processing of fetched data and continue of loading - continueLoading = response.code != 304 && nsIncomingDataProcessor.processSgvs(sgvs, nsClientV3Plugin.doingFullSync) + continueLoading = !cursorStalled && nsIncomingDataProcessor.processSgvs(sgvs, nsClientV3Plugin.doingFullSync) } else { // End first load if (isFirstLoad) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorker.kt index 9a0a0e18175d..74ed1b66e9aa 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorker.kt @@ -47,12 +47,18 @@ class LoadTreatmentsWorker @AssistedInject constructor( if ((nsClientV3Plugin.newestDataOnServer?.collections?.treatments ?: Long.MAX_VALUE) > lastLoaded) { val treatments: List val response: NSAndroidClient.ReadResponse>? + // Replaces the old `response.code != 304` brake - see the same change in + // LoadBgWorker. The 304 came from OkHttp's disk cache revalidating a GET; what it + // meant is checked directly now, namely that the server has nothing newer so the + // cursor cannot move and the next page would repeat this one. + var cursorStalled = false if (isFirstLoad) { val lastLoadedIso = dateUtil.toISOString(lastLoaded) response = nsAndroidClient.getTreatmentsNewerThan(lastLoadedIso, NSClientV3Plugin.RECORDS_TO_LOAD) } else { response = nsAndroidClient.getTreatmentsModifiedSince(lastLoaded, NSClientV3Plugin.RECORDS_TO_LOAD) aapsLogger.debug(LTag.NSCLIENT, "lastLoadedSrvModified: ${response.lastServerModified}") + cursorStalled = response.lastServerModified?.let { it <= lastLoaded } == true response.lastServerModified?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.treatments = it } nsClientV3Plugin.storeLastLoadedSrvModified() } @@ -63,7 +69,7 @@ class LoadTreatmentsWorker @AssistedInject constructor( nsClientRepository.addLog("◄ $action", "${treatments.size} TRs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}") // Schedule processing of fetched data and continue of loading continueLoading = - response.code != 304 && nsIncomingDataProcessor.processTreatments(response.values, nsClientV3Plugin.doingFullSync) + !cursorStalled && nsIncomingDataProcessor.processTreatments(response.values, nsClientV3Plugin.doingFullSync) } else { // End first load if (isFirstLoad) { From 37e146861ff70e0fa6e54dd4a271fdc1719b451f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 18:05:00 +0200 Subject: [PATCH 010/146] 4.0.0-dev-b-kmp --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 204039ec9942..8b9ded63720f 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget object Versions { // On change edit aaps-ci.yml - const val appVersion = "4.0.0-dev-b" + const val appVersion = "4.0.0-dev-b-kmp" const val versionCode = 1500 const val compileSdk = 37 From e44f2f8c67077ba22c3a7678c82019cd819c64ae Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 6 Aug 2026 18:39:04 +0200 Subject: [PATCH 011/146] :core:nssdk kmp --- _docs/KMP_IOS_FEASIBILITY.md | 206 +++++++- core/nssdk/build.gradle.kts | 86 +-- .../core/nssdk/NSAndroidCallbackClientImpl.kt | 4 +- .../aaps/core/nssdk/NSAndroidClientImpl.kt | 4 +- .../kotlin/app/aaps/core/nssdk/NsSdkJson.kt | 0 .../DateHeaderOutOfToleranceException.kt | 0 .../exceptions/InvalidAccessTokenException.kt | 0 .../InvalidFormatNightscoutException.kt | 0 .../InvalidParameterNightscoutException.kt | 0 .../nssdk/exceptions/NightscoutException.kt | 0 .../UnknownResponseNightscoutException.kt | 0 .../UnsuccessfulNightscoutException.kt | 0 .../interfaces/NSAndroidCallbackClient.kt | 0 .../core/nssdk/interfaces/NSAndroidClient.kt | 0 .../nssdk/interfaces/RunningConfiguration.kt | 0 .../core/nssdk/localmodel/ApiPermission.kt | 1 + .../core/nssdk/localmodel/ApiPermissions.kt | 1 + .../app/aaps/core/nssdk/localmodel/Status.kt | 0 .../app/aaps/core/nssdk/localmodel/Storage.kt | 0 .../localmodel/clientcontrol/AckEnvelope.kt | 0 .../clientcontrol/AuthorizedClient.kt | 0 .../localmodel/clientcontrol/BolusPreview.kt | 0 .../clientcontrol/ClientControlMessage.kt | 0 .../localmodel/clientcontrol/MasterPairing.kt | 0 .../localmodel/clientcontrol/PairingOffer.kt | 0 .../clientcontrol/PairingPayload.kt | 0 .../clientcontrol/ProgressEnvelope.kt | 0 .../clientcontrol/SignedEnvelope.kt | 0 .../configuration/NSRunningConfiguration.kt | 0 .../localmodel/devicestatus/NSDeviceStatus.kt | 0 .../core/nssdk/localmodel/entry/Direction.kt | 0 .../core/nssdk/localmodel/entry/NSMbgV3.kt | 0 .../core/nssdk/localmodel/entry/NSSgvV3.kt | 0 .../core/nssdk/localmodel/entry/NsUnits.kt | 0 .../aaps/core/nssdk/localmodel/food/NSFood.kt | 0 .../treatment/CreateUpdateResponse.kt | 0 .../nssdk/localmodel/treatment/EventType.kt | 1 + .../nssdk/localmodel/treatment/NSBolus.kt | 0 .../localmodel/treatment/NSBolusWizard.kt | 0 .../nssdk/localmodel/treatment/NSCarbs.kt | 0 .../treatment/NSEffectiveProfileSwitch.kt | 0 .../localmodel/treatment/NSExtendedBolus.kt | 0 .../core/nssdk/localmodel/treatment/NSICfg.kt | 0 .../localmodel/treatment/NSOfflineEvent.kt | 0 .../localmodel/treatment/NSProfileSwitch.kt | 0 .../localmodel/treatment/NSTemporaryBasal.kt | 0 .../localmodel/treatment/NSTemporaryTarget.kt | 0 .../localmodel/treatment/NSTherapyEvent.kt | 1 + .../nssdk/localmodel/treatment/NSTreatment.kt | 1 + .../core/nssdk/mapper/ApiPermissionMapper.kt | 0 .../core/nssdk/mapper/DeviceStatusMapper.kt | 0 .../app/aaps/core/nssdk/mapper/FoodMapper.kt | 2 +- .../app/aaps/core/nssdk/mapper/ICfgMapper.kt | 0 .../app/aaps/core/nssdk/mapper/MbgMapper.kt | 2 +- .../core/nssdk/mapper/StatusResponseMapper.kt | 0 .../aaps/core/nssdk/mapper/StorageMapper.kt | 0 .../app/aaps/core/nssdk/mapper/SvgMapper.kt | 2 +- .../aaps/core/nssdk/mapper/TreatmentMapper.kt | 2 +- .../core/nssdk/networking/NightscoutApi.kt | 0 .../app/aaps/core/nssdk/networking/NsAuth.kt | 5 +- .../core/nssdk/networking/NsHttpResponse.kt | 0 .../core/nssdk/networking/NsKtorClient.kt | 34 +- .../app/aaps/core/nssdk/networking/NsUrl.kt | 0 .../app/aaps/core/nssdk/networking/Status.kt | 0 .../core/nssdk/remotemodel/LastModified.kt | 0 .../nssdk/remotemodel/RemoteAuthResponse.kt | 0 .../nssdk/remotemodel/RemoteDeviceStatus.kt | 6 +- .../core/nssdk/remotemodel/RemoteEntry.kt | 0 .../aaps/core/nssdk/remotemodel/RemoteFood.kt | 0 .../aaps/core/nssdk/remotemodel/RemoteICfg.kt | 0 .../nssdk/remotemodel/RemoteStatusResponse.kt | 0 .../core/nssdk/remotemodel/RemoteTreatment.kt | 29 +- .../aaps/core/nssdk/remotemodel/examples.json | 489 +++++++++++++++++ .../aaps/core/nssdk/utils/CoroutineUtils.kt | 2 +- .../app/aaps/core/nssdk/utils/IoDispatcher.kt | 12 + .../app/aaps/core/nssdk/utils/ListUtils.kt | 5 + .../core/nssdk/networking/NsHttpClient.jvm.kt | 33 ++ .../core/nssdk/utils/ClientControlCrypto.kt | 2 + .../nssdk/utils/ClientControlPairingCrypto.kt | 0 .../aaps/core/nssdk/utils/IoDispatcher.jvm.kt | 7 + .../aaps/core/nssdk/NsSdkWireFormatTest.kt | 0 .../exceptions/NightscoutExceptionTest.kt | 0 .../nssdk/mapper/ApiPermissionMapperTest.kt | 0 .../core/nssdk/mapper/CreatedAtParsingTest.kt | 0 .../nssdk/mapper/DeviceStatusMapperTest.kt | 0 .../nssdk/mapper/FoodAndEntryToleranceTest.kt | 0 .../nssdk/mapper/KotlinxCoercionSpikeTest.kt | 0 .../aaps/core/nssdk/mapper/MbgMapperTest.kt | 0 .../mapper/RealNightscoutTreatmentTest.kt | 0 .../nssdk/mapper/StatusResponseMapperTest.kt | 0 .../core/nssdk/mapper/WireTypeCoercionTest.kt | 4 +- .../nssdk/networking/NightscoutApiUrlTest.kt | 0 .../nssdk/networking/NsSdkAuthContractTest.kt | 0 .../networking/NsSdkErrorBodyContractTest.kt | 0 .../networking/NsSdkResponseContractTest.kt | 0 .../networking/NsSdkStatusContractTest.kt | 0 .../nssdk/networking/NsSdkUrlContractTest.kt | 0 .../nssdk/utils/ClientControlCryptoTest.kt | 0 .../utils/ClientControlCryptoVectorsTest.kt | 0 .../utils/ClientControlPairingCryptoTest.kt | 0 core/nssdk/src/main/AndroidManifest.xml | 4 - .../aaps/core/nssdk/remotemodel/examples.json | 490 ------------------ .../app/aaps/core/nssdk/utils/ListUtils.kt | 4 - .../nssdk/networking/NsHttpClient.mingwX64.kt | 21 + .../core/nssdk/utils/IoDispatcher.mingwX64.kt | 12 + gradle/libs.versions.toml | 1 + 106 files changed, 880 insertions(+), 593 deletions(-) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt (88%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/NsSdkJson.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/DateHeaderOutOfToleranceException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/InvalidAccessTokenException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/InvalidFormatNightscoutException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/InvalidParameterNightscoutException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/UnknownResponseNightscoutException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/exceptions/UnsuccessfulNightscoutException.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidCallbackClient.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/Status.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/Storage.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AckEnvelope.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AuthorizedClient.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/BolusPreview.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ClientControlMessage.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/MasterPairing.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingOffer.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingPayload.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ProgressEnvelope.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/SignedEnvelope.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/configuration/NSRunningConfiguration.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/devicestatus/NSDeviceStatus.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/entry/Direction.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/entry/NSMbgV3.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/entry/NSSgvV3.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/entry/NsUnits.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/food/NSFood.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/CreateUpdateResponse.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolus.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolusWizard.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSCarbs.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSEffectiveProfileSwitch.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSExtendedBolus.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSICfg.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSOfflineEvent.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSProfileSwitch.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryBasal.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryTarget.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt (99%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/ICfgMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/StorageMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt (95%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt (80%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/networking/Status.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt (95%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt (100%) rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt (81%) create mode 100644 core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json rename core/nssdk/src/{main => commonMain}/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt (89%) create mode 100644 core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.kt create mode 100644 core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt create mode 100644 core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.jvm.kt rename core/nssdk/src/{main => jvmMain}/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt (97%) rename core/nssdk/src/{main => jvmMain}/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCrypto.kt (100%) create mode 100644 core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.jvm.kt rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapperTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/MbgMapperTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapperTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt (100%) rename core/nssdk/src/{test => jvmTest}/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCryptoTest.kt (100%) delete mode 100644 core/nssdk/src/main/AndroidManifest.xml delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/examples.json delete mode 100644 core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt create mode 100644 core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.mingwX64.kt create mode 100644 core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.mingwX64.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 9238af4254cf..0c756ecfa7ed 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -57,7 +57,7 @@ So most of the Compose work of the last year can be reused. |--------------------------------------------------------|--------------------------------------------------------|----------------------------------------| | Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | | RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | -| Retrofit + OkHttp | 12 / 14, of which 4 / 3 in `:core:nssdk` | Ktor client | +| Retrofit + OkHttp | 9 / 11, **0 in `:core:nssdk`** | Ktor client - done there, see section 8 | | socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | | Gson | 46, **0 in `:core:nssdk`** | kotlinx.serialization - see section 8 | | `org.json` (`JSONObject`) | 227, **0 in `:core:nssdk`** | kotlinx `JsonObject` - see section 8 | @@ -373,23 +373,20 @@ Step 0 proves the toolchain works, gives an honest answer about how Compose Mult iOS, and makes every later step demand driven: a blocker is removed because it stands between you and the next screen, not because it is on a list. -**Where this stands.** Step 1 is done. Half of step 0 is done too, and out of order: `:core:data` -already builds for Kotlin/Native (wave 5), and most of step 5 has been pulled forward because -`:core:nssdk` turned out to be sliceable after all - `org.json` in wave 6, Gson in wave 7. What is -left of that module is the HTTP client itself: +**Where this stands.** Steps 1 and 5 are **done**, and step 0 is half done - out of order, because +`:core:nssdk` turned out to be sliceable after all. Two modules now build for Kotlin/Native: -| Left in `:core:nssdk` | Files | +| Module | State | | --- | --- | -| Retrofit / OkHttp | 4 / 3 | -| `java.io.IOException` (the exception hierarchy) | 1 | -| `android.*` (mainly `Context` for the OkHttp cache) | 2 | -| joda-time (a post-decode helper, not the wire contract) | 1 | +| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | +| `:core:nssdk` | multiplatform, 72 files in `commonMain` (waves 6-9) | -All four go together with Ktor, which makes the rest of step 5 one piece of work rather than four. +Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Gson, joda, Retrofit, +OkHttp, `android.*` and `java.io` are all gone from it. -What is still missing from step 0 is the half that needs a Mac - a real device, and an honest look at -Compose Multiplatform on iOS. No amount of further blocker removal answers that question, which is -the argument for doing it soon rather than continuing down the list. +That leaves steps 2, 3, 4 and 6, and the half of step 0 that **needs a Mac**: a real device, and an +honest look at how Compose Multiplatform feels on iOS. No amount of further blocker removal answers +that question, which is the argument for doing it soon rather than continuing down the list. --- @@ -414,6 +411,10 @@ worth keeping (see waves 5 and 6): | `e24476237b` | Prepare `:core:nssdk` - dead code out, defaults in, characterization tests | | `f6a4e85e8a` | `:core:nssdk` `org.json` -> kotlinx | | `350f486be4` | `:core:nssdk` Gson -> kotlinx.serialization | +| `cb7b8fa924` | `:core:nssdk` date parsing, eliminate joda | +| `e92ec082d3` | `:core:nssdk` tests - the contract suite, written against Retrofit before the port | +| `ed9c87599f` | `:core:nssdk` Ktor migration | +| `37e146861f` | version `4.0.0-dev-b-kmp` | ### Wave 1 - `DecimalFormat` removed @@ -859,6 +860,152 @@ Not covered on the device: temp basal and extended bolus (need real pump or loop **food upload path does not exist** - `QueueCounter` has no food counter, AAPS syncs food download-only. The food fix rests on unit tests. +### Wave 8 - `:core:nssdk` off Retrofit and OkHttp (done) + +The HTTP client moved to **Ktor on the OkHttp engine**, so the app reuses the OkHttp it already ships +rather than carrying two HTTP stacks. On iOS the engine becomes Darwin and nothing else changes, +which is the whole reason for the move. + +#### The map came first, and it was worth it + +Before any code changed, five parallel readers mapped what the Retrofit stack actually guaranteed, +and each finding was then attacked by an agent trying to refute it. That produced a written contract +and **30 silent-failure risks**. Three were serious enough to have shipped: + +- **The 304 signal only existed because of the OkHttp disk cache.** `response.raw().networkResponse + ?.code` can only be 304 when the cache revalidates a GET and merges the result into a 200. Ktor has + no equivalent, so `code` would have become a constant 200 - and `LoadBgWorker`'s + `response.code != 304 && processSgvs(...)` would have become an unconditional process. The paging + loop never exits, `storeGlucoseValuesToDb()` is never reached, and **BG never lands in the + database** while the worker looks busy. +- **Static query pairs are hidden inside the `@GET` strings**, not in `@Query` parameters - + `v3/profile?sort$desc=date&limit=1`. Rebuild the URL from the method parameters and they vanish. + Losing `limit=1` there makes `LoadProfileStoreWorker` take `profiles[profiles.size - 1]` from an + unsorted list, silently applying the **wrong profile**: different basal, ISF and IC. +- **The `utcOffset` auto-retry reads the raw 400 body text.** Ktor has no separate `errorBody()`, and + a body read lazily or only on success loses that string. The record is then dropped permanently + while the sync cursor advances. + +#### Tests before code, against the old stack + +49 characterization tests were written **while Retrofit was still in place**, using **MockWebServer** +- a real HTTP server on localhost - rather than Ktor's `MockEngine`. That choice is the point: +`MockEngine` only exists after the swap, so tests written with it could never have proved anything +about the behaviour before it. The same files ran unchanged against both stacks. + +They pin: every endpoint URL as a literal string; the read/write status asymmetry and its **request +counts**; the `utcOffset` fallback verified by inspecting both request bodies; the auth headers, +refresh and clock-skew paths; ETag parsing; and the `{"result": ...}` envelope, which is applied +inconsistently per endpoint. + +Two divergences were caught this way rather than in the field: + +| | | +| --- | --- | +| `$` in query keys | survived on the first try - Ktor's `encodedParameters` leaves it literal | +| `/` inside a path segment | Ktor does **not** encode it, Retrofit does (`a%2Fb`). Fixed with `encodeSlash = true` | + +#### Decisions worth recording + +- **`expectSuccess = false`.** Ktor's default throws on any non-2xx, which would make the whole 4xx + ladder dead code - and because `ClientRequestException` is not in the retry exclusion list, every + such call would also be retried four times before surfacing. +- **No `HttpRequestRetry`.** It would multiply with the existing `retry()`, and the `utcOffset` + fallback re-enters a public method, so one reading could produce over ten POSTs in a single sync. +- **Auth is hand written, not Ktor's `bearer` provider.** The provider differs in four ways, two of + which lose data quietly: it omits the header when it has no token (Nightscout then answers **200 + with the anonymous role**, and uploads stop with nothing logged), and it refreshes on 401 only, + while Nightscout also answers 403. It also never sees the response body, so the clock-skew error + could not exist. +- **The 304 brake was replaced, not reproduced.** The workers now stop when the cursor cannot advance + (`lastServerModified <= lastLoaded`), which is what the 304 always meant. Only the modified-since + path can stall that way; the first load pages by date and carries no server timestamp. +- **`close()` was added** to `NSAndroidClient` and is called from `restartOnChange`. The Retrofit + client leaked quietly on every URL or token change; a Ktor engine holds real connections. + +Also fixed on the way, deliberately and separately: `getVersion` / `getStatus` threw +`retrofit2.HttpException` and `NullPointerException`, neither a `NightscoutException`; they now throw +`UnsuccessfulNightscoutException`. And a 10 s connect timeout is now explicit - OkHttp applied one by +default and Ktor does not, so a dead host would otherwise hang for the full 60 s socket timeout. + +**Left broken on purpose:** `updateFood` and `deleteFood` declare an `{identifier}` the path does not +contain, so Retrofit refused to build them and **no request was ever sent** - food edits have never +synced, because the endpoint is broken on the Nightscout side. A hand-written Ktor URL would have +turned "never sends" into `PATCH /api/v3/food` and `DELETE /api/v3/food` **with no identifier**, a +request against the whole collection on a live server. They reproduce the local failure instead, +verified by a test asserting zero requests. + +#### Verified on a device + +A new Ktor master against a **pre-KMP client** on a live Nightscout: writes accepted (201), reads +across status / lastModified / settings / devicestatus / treatments, socket.io push received, and the +old Gson/Retrofit client stored what Ktor wrote. The auth flow ran for real - a 401 triggered +`GET /api/v2/authorization/request/` and a 200 - which exercised the hand-written interceptor +and the leading-slash refresh URL together. + +Turning the WebSocket **off** was needed to see any of the REST paging at all: with push enabled the +load workers barely run. With it off, `◄ RCV TR END` on the modified-since path confirmed the new +cursor brake terminates instead of spinning. + +Not observed on the wire: the `$` operators, because `date$gt` / `created_at$gt` / `sort$desc` only +appear on **first-load** paths and every collection was already synced. They are covered by two test +files asserting the same literal URLs against Retrofit and against Ktor, so the bytes are identical +and Nightscout cannot tell them apart. + +### Wave 9 - `:core:nssdk` is multiplatform (done) + +Same shape as `:core:data`: `jvm()` plus `mingwX64()` as the Kotlin/Native compile proof. **72 files +in `commonMain`**, and all four consumer modules build unchanged. + +Only four things blocked `commonMain`, and none needed design work: + +| Blocker | Fix | +| --- | --- | +| `@JvmSynthetic` on an `internal` helper | dropped - it was hiding an already-internal function from Java | +| `KClass` in `retry` | `KClass`. Exact-class matching and the `catch (Exception)` are unchanged, so which exceptions are excluded and how many attempts happen stay identical | +| `Dispatchers.IO`, 2 sites | `expect val nsIoDispatcher`; the JVM actual is the same `Dispatchers.IO` as before | +| the Ktor engine and its logging interceptor | `expect fun nsHttpClient(...)` - OkHttp on JVM, CIO on Native | + +#### The crypto did not need solving + +`ClientControlCrypto` and `ClientControlPairingCrypto` are `javax.crypto` and `java.security`, and +they looked like the blocker. They are not: **nothing inside the module calls them** - the only +internal reference is a KDoc link - so they simply live in `jvmMain`. No `expect` / `actual`, no +stub, no crypto library, and the Native target compiles without them. + +The code stays in `:core:nssdk`, declared as its JVM part. When client-control is wanted on another +platform it is a source-set move **within this module** plus actuals, not a redesign. + +Golden vectors were extracted first, in `ClientControlCryptoVectorsTest`: fixed inputs with their +exact outputs, so a second implementation can be checked byte for byte rather than by "it is also +HMAC-SHA256". The algorithms are standards and interoperate by definition; the **packaging** is what +differs and what fails silently: + +- **GCM tag placement** - JCE returns `ciphertext ‖ tag` from one `doFinal`; Apple's CryptoKit keeps + the tag separate. The vectors pin `wrapped.size == plaintext.size + 16`. +- **Hex case** - signatures are compared as text, so lower case is part of the wire format. +- **PIN encoding** - ASCII digits today, which is why any encoding agrees. Worth knowing before + anyone widens the alphabet. + +Those values cannot change without breaking every deployed AAPS, so a failure there is a real +incompatibility, not a stale test. + +#### What a desktop JVM target would cost + +Nothing, for the crypto. The pattern here is a plain `jvm()` target, not `androidTarget()`, so the +single `jvmMain` already serves Android - and `javax.crypto` is Java SE, so Windows, Linux and macOS +desktop builds use the same code untouched. Only Kotlin/Native targets need a second implementation. + +**Web would be different**, and it is a decision to take early rather than late: WebCrypto is +async-only, so `sign()` and `wrap()` would have to become `suspend`, and retrofitting that ripples +through every caller. + +#### Honest limits of the proof + +`mingwX64` is a **compile proof, not a shipping target**: request logging is not wired up there, and +CIO stands in for what an iOS build would use (Darwin). What it does prove is real - 72 files of wire +layer, models, mappers and HTTP client with no JVM API anywhere in them. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old @@ -929,8 +1076,10 @@ keeps the old contract on a public interface method. characterization tests could actually do their job on the serialization half. The throwaway dependency cost nothing in the end: Retrofit 3.0.0 ships an official `converter-kotlinx-serialization`, so it was one catalog line and no third party code. -9. **Still open: the OkHttp disk cache needs a `Context`.** One of the two remaining `android.*` - imports in `:core:nssdk`. Ktor on iOS needs either a different cache story or none. +9. ~~The OkHttp disk cache needs a `Context`.~~ **Resolved: there is no cache.** It existed only so a + revalidated GET could surface as a 304, which the paging workers used as their stop condition. + That is now expressed directly - stop when the cursor cannot advance - so the cache, the `Context` + and the two `Cache` instances that shared one directory are all gone. See wave 8. 10. ~~R8 has never run.~~ **Not a risk: AAPS does not minify.** Both convention plugins (`android-app-dependencies` and `android-module-dependencies`) set `isMinifyEnabled = false` for the `release` build type, so R8 never shrinks or obfuscates and the usual @@ -939,13 +1088,24 @@ keeps the old contract on a public interface method. converter are present in the release dex. The `benchmark` variant (`initWith(release)` plus debug signing) installs and starts clean; its runtime sync check is still outstanding because the emulator lost DNS, which starved both apps equally. -11. **Still open: merge `kmp/core-data-experiment` into `dev`.** Waves 5 to 7 all live there. Nothing - argues against it any more - the branch was device-verified in mixed-version pairs - but it is a - real merge of a wire format change and deserves its own decision. - -Waves 1 to 4 are committed on `dev`. Waves 5 to 7 are committed on `kmp/core-data-experiment`, all -verified against a live Nightscout - waves 5 and 6 on WSA, wave 7 on an emulator running a new master -against a pre-KMP client. The `FoodManagement` comma defect in section 10 is found but not fixed. +11. **Still open: merge `kmp/core-data-experiment` into `dev`.** Waves 5 to 9 all live there. Nothing + argues against it any more - the branch was device-verified in mixed-version pairs at each step - + but it is a real merge of a wire format change and deserves its own decision. It gets riskier the + longer it waits: `dev` has not moved yet, so the merge is still a fast-forward. +12. **Still open: client-control crypto on a non-JVM platform.** It sits in `jvmMain` and nothing in + `commonMain` calls it, so no target needs it today. A **follower** never signs commands or + unwraps pairing offers, so this only becomes real when another platform should *send* commands. + Golden vectors are ready; the choice is then `expect` / `actual` with a hand written Apple + implementation, or cryptography-kotlin. Prefer deciding it when the platform exists, not before. +13. **Still open: does web ever matter?** It is the one target that changes the **API shape** rather + than just the implementation - WebCrypto is async-only, so `sign()` and `wrap()` would have to + become `suspend`. Cheap to decide now, expensive to retrofit. + +Waves 1 to 4 are committed on `dev`. Waves 5 to 9 are committed on `kmp/core-data-experiment`, each +verified against a live Nightscout before the next one started - waves 5 and 6 on WSA, waves 7 to 9 +on an emulator running a new master against a **pre-KMP client**, so every step was checked in a +mixed-version pair rather than only against itself. The `FoodManagement` comma defect in section 10 +is found but not fixed. --- diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 61dbefa542c5..f5552668b264 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -1,36 +1,64 @@ plugins { - alias(libs.plugins.android.library) + kotlin("multiplatform") id("kotlinx-serialization") - id("android-module-dependencies") - id("test-module-dependencies") - id("jacoco-module-dependencies") } -android { - namespace = "app.aaps.core.nssdk" -} - -dependencies { - api(libs.com.squareup.okhttp3.okhttp) - - // Ktor on the OkHttp engine: reuses the OkHttp already in the app rather than adding a second - // HTTP stack. The engine becomes Darwin when this module builds for iOS. - api(libs.io.ktor.client.core) - implementation(libs.io.ktor.client.okhttp) - implementation(libs.io.ktor.client.content.negotiation) - implementation(libs.io.ktor.serialization.kotlinx.json) - api(libs.kotlinx.datetime) +kotlin { + jvm { + // Every consumer of :core:nssdk is still an Android module, and they resolve this variant. + // Nothing about them changes. + compilerOptions { + jvmTarget.set(Versions.jvmTarget) + } + } - // Test only: a real HTTP server on localhost, so the same characterization tests run against - // Retrofit today and Ktor after the port. Ktor MockEngine could not do that - it only exists - // after the swap, so it could never pin the OLD behaviour. - testImplementation(libs.com.squareup.okhttp3.mockwebserver) + // Stand-in for a real Kotlin/Native target, same as :core:data. iOS needs macOS and Xcode, but + // mingwX64 compiles the same common code through Kotlin/Native, which is what proves there is no + // JVM API left in commonMain. Replace or extend with iosArm64() / iosSimulatorArm64() on a Mac. + mingwX64() - api(libs.kotlin.stdlib.jdk8) + sourceSets { + val commonMain by getting { + dependencies { + api(libs.io.ktor.client.core) + implementation(libs.io.ktor.client.content.negotiation) + implementation(libs.io.ktor.serialization.kotlinx.json) + api(libs.kotlinx.datetime) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.json) + } + } + val jvmMain by getting { + dependencies { + // OkHttp is both the Ktor engine and, on the JVM side, what the rest of the app + // already uses. The client-control crypto in this source set is javax.crypto. + api(libs.com.squareup.okhttp3.okhttp) + implementation(libs.io.ktor.client.okhttp) + } + } + val mingwX64Main by getting { + dependencies { + // CIO is Ktor's own multiplatform engine, enough for the compile proof. An Apple + // target would use ktor-client-darwin instead. + implementation(libs.io.ktor.client.cio) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.org.junit.jupiter) + implementation(libs.org.junit.jupiter.api) + runtimeOnly(libs.org.junit.platform.launcher) + implementation(libs.com.google.truth) + implementation(libs.org.mockito.kotlin) + implementation(libs.kotlinx.coroutines.test) + // A real HTTP server on localhost, so the contract tests exercise the whole stack. + implementation(libs.com.squareup.okhttp3.mockwebserver) + } + } + } +} - api(platform(libs.kotlinx.coroutines.bom)) - api(libs.kotlinx.coroutines.core) - runtimeOnly(libs.kotlinx.coroutines.android) - api(platform(libs.kotlinx.serialization.bom)) - api(libs.kotlinx.serialization.json) -} \ No newline at end of file +tasks.withType { + useJUnitPlatform() +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt similarity index 88% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt index e971cd57e253..c9e37eb4660f 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidCallbackClientImpl.kt @@ -3,15 +3,15 @@ package app.aaps.core.nssdk import app.aaps.core.nssdk.interfaces.NSAndroidCallbackClient import app.aaps.core.nssdk.interfaces.NSAndroidClient import app.aaps.core.nssdk.localmodel.Status +import app.aaps.core.nssdk.utils.nsIoDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class NSAndroidCallbackClientImpl(private val client: NSAndroidClient) : NSAndroidCallbackClient { - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val scope = CoroutineScope(nsIoDispatcher + SupervisorJob()) @Suppress("TooGenericExceptionCaught") override fun getStatus(callback: NSAndroidCallbackClient.NSCallback): NSAndroidCallbackClient.NSCancellable = diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt index b1a3b3415221..14785c123db3 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NSAndroidClientImpl.kt @@ -32,10 +32,10 @@ import app.aaps.core.nssdk.remotemodel.RemoteEntry import app.aaps.core.nssdk.remotemodel.RemoteFood import app.aaps.core.nssdk.remotemodel.RemoteStatusResponse import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import app.aaps.core.nssdk.utils.nsIoDispatcher import app.aaps.core.nssdk.utils.retry import app.aaps.core.nssdk.utils.toNotNull import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive @@ -66,7 +66,7 @@ class NSAndroidClientImpl( accessToken: String, logging: Boolean, logger: (String) -> Unit, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO + private val dispatcher: CoroutineDispatcher = nsIoDispatcher ) : NSAndroidClient { private val stack = NsKtorClient.stack( diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NsSdkJson.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/NsSdkJson.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/NsSdkJson.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/DateHeaderOutOfToleranceException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/DateHeaderOutOfToleranceException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/DateHeaderOutOfToleranceException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/DateHeaderOutOfToleranceException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidAccessTokenException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidAccessTokenException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidAccessTokenException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidAccessTokenException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidFormatNightscoutException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidFormatNightscoutException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidFormatNightscoutException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidFormatNightscoutException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidParameterNightscoutException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidParameterNightscoutException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/InvalidParameterNightscoutException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/InvalidParameterNightscoutException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/NightscoutException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/UnknownResponseNightscoutException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/UnknownResponseNightscoutException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/UnknownResponseNightscoutException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/UnknownResponseNightscoutException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/UnsuccessfulNightscoutException.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/UnsuccessfulNightscoutException.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/exceptions/UnsuccessfulNightscoutException.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/exceptions/UnsuccessfulNightscoutException.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidCallbackClient.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidCallbackClient.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidCallbackClient.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidCallbackClient.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/NSAndroidClient.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/interfaces/RunningConfiguration.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt index df83ecb989ac..a476b8d97ea2 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermission.kt @@ -6,6 +6,7 @@ data class ApiPermission( val update: Boolean, val delete: Boolean ) { + val full: Boolean get() = this.create && this.read && this.update && this.delete } diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt index 84266d0155ec..cf0e255a9ca9 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/ApiPermissions.kt @@ -8,6 +8,7 @@ data class ApiPermissions( val settings: ApiPermission, val treatments: ApiPermission ) { + fun isFull() = deviceStatus.full && entries.full && food.full && profile.full && settings.full && treatments.full fun isRead() = deviceStatus.read && entries.read && food.read && profile.read && settings.read && treatments.read } diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/Status.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/Status.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/Status.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/Status.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/Storage.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/Storage.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/Storage.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/Storage.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AckEnvelope.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AckEnvelope.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AckEnvelope.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AckEnvelope.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AuthorizedClient.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AuthorizedClient.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AuthorizedClient.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/AuthorizedClient.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/BolusPreview.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/BolusPreview.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/BolusPreview.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/BolusPreview.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ClientControlMessage.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ClientControlMessage.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ClientControlMessage.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ClientControlMessage.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/MasterPairing.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/MasterPairing.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/MasterPairing.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/MasterPairing.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingOffer.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingOffer.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingOffer.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingOffer.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingPayload.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingPayload.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingPayload.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/PairingPayload.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ProgressEnvelope.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ProgressEnvelope.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ProgressEnvelope.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/ProgressEnvelope.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/SignedEnvelope.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/SignedEnvelope.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/SignedEnvelope.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/clientcontrol/SignedEnvelope.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/configuration/NSRunningConfiguration.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/configuration/NSRunningConfiguration.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/configuration/NSRunningConfiguration.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/configuration/NSRunningConfiguration.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/devicestatus/NSDeviceStatus.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/devicestatus/NSDeviceStatus.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/devicestatus/NSDeviceStatus.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/devicestatus/NSDeviceStatus.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/Direction.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/Direction.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/Direction.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/Direction.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NSMbgV3.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NSMbgV3.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NSMbgV3.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NSMbgV3.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NSSgvV3.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NSSgvV3.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NSSgvV3.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NSSgvV3.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NsUnits.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NsUnits.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/entry/NsUnits.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/entry/NsUnits.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/food/NSFood.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/food/NSFood.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/food/NSFood.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/food/NSFood.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/CreateUpdateResponse.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/CreateUpdateResponse.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/CreateUpdateResponse.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/CreateUpdateResponse.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt index 5964bbffaea9..6c627627ab88 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/EventType.kt @@ -41,6 +41,7 @@ enum class EventType(val text: String) { @SerialName("") NONE(""); companion object { + fun fromString(text: String?) = entries.firstOrNull { it.text == text } ?: NONE } } \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolus.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolus.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolus.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolus.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolusWizard.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolusWizard.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolusWizard.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSBolusWizard.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSCarbs.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSCarbs.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSCarbs.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSCarbs.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSEffectiveProfileSwitch.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSEffectiveProfileSwitch.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSEffectiveProfileSwitch.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSEffectiveProfileSwitch.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSExtendedBolus.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSExtendedBolus.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSExtendedBolus.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSExtendedBolus.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSICfg.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSICfg.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSICfg.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSICfg.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSOfflineEvent.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSOfflineEvent.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSOfflineEvent.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSOfflineEvent.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSProfileSwitch.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSProfileSwitch.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSProfileSwitch.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSProfileSwitch.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryBasal.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryBasal.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryBasal.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryBasal.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryTarget.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryTarget.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryTarget.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTemporaryTarget.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt index 937c9f9e3d06..62861e5ad29d 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTherapyEvent.kt @@ -35,6 +35,7 @@ data class NSTherapyEvent( @Serializable enum class MeterType(val text: String) { + @SerialName("Finger") FINGER("Finger"), @SerialName("Sensor") SENSOR("Sensor"), @SerialName("Manual") MANUAL("Manual") diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt similarity index 99% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt index 38c4802e9e9c..55a1ea26fe18 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/localmodel/treatment/NSTreatment.kt @@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonClassDiscriminator @Serializable @JsonClassDiscriminator("__type") sealed interface NSTreatment { + var date: Long? val device: String? val identifier: String? diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapper.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapper.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt index 5dfba298f147..bc5df014716b 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/FoodMapper.kt @@ -1,8 +1,8 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.food.NSFood -import app.aaps.core.nssdk.remotemodel.RemoteFood import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteFood /** * Convert to [RemoteFood] and back to [NSFood] diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/ICfgMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/ICfgMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/ICfgMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/ICfgMapper.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt index 3c01db59848f..95d2f4a7cedd 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/MbgMapper.kt @@ -2,8 +2,8 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.entry.NSMbgV3 import app.aaps.core.nssdk.localmodel.entry.NsUnits -import app.aaps.core.nssdk.remotemodel.RemoteEntry import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteEntry fun String.toCalibrationMbg(): NSMbgV3? = nsSdkJson.decodeFromString(RemoteEntry.serializer(), this).toCalibrationMbg() diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapper.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/StorageMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/StorageMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/StorageMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/StorageMapper.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt index f3ac4024fa19..94557517a306 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/SvgMapper.kt @@ -3,8 +3,8 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.entry.Direction import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 import app.aaps.core.nssdk.localmodel.entry.NsUnits -import app.aaps.core.nssdk.remotemodel.RemoteEntry import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteEntry fun NSSgvV3.convertToRemoteAndBack(): NSSgvV3? = toRemoteEntry().toSgv() diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt index 8a4a36521748..a0ac93fd85ab 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/mapper/TreatmentMapper.kt @@ -13,8 +13,8 @@ import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryBasal import app.aaps.core.nssdk.localmodel.treatment.NSTemporaryTarget import app.aaps.core.nssdk.localmodel.treatment.NSTherapyEvent import app.aaps.core.nssdk.localmodel.treatment.NSTreatment -import app.aaps.core.nssdk.remotemodel.RemoteTreatment import app.aaps.core.nssdk.nsSdkJson +import app.aaps.core.nssdk.remotemodel.RemoteTreatment import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NightscoutApi.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt similarity index 95% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt index 3aaa431cc89d..129c4d82cd76 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsAuth.kt @@ -15,6 +15,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.URLBuilder import io.ktor.http.appendPathSegments import io.ktor.http.takeFrom +import kotlin.time.Clock /** * The Nightscout token dance, as an `HttpSend` interceptor. @@ -99,7 +100,9 @@ internal object NsAuth { appendPathSegments(listOf("api", "v2", "authorization", "request", refreshToken), encodeSlash = true) }.buildString() - private fun nowMillis(): Long = System.currentTimeMillis() + // kotlin.time.Clock rather than System.currentTimeMillis(), which is JVM only. kotlinx-datetime + // is already a dependency of this module. + private fun nowMillis(): Long = Clock.System.now().toEpochMilliseconds() /** Nightscout answers 403 as well as 401 for an expired token. */ private val REFRESHABLE = listOf(401, 403) diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsHttpResponse.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt similarity index 80% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt index 9a6f1372d4e1..d9179ac067a5 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsKtorClient.kt @@ -2,17 +2,27 @@ package app.aaps.core.nssdk.networking import app.aaps.core.nssdk.nsSdkJson import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.HttpClientConfig import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json /** - * Builds the Ktor client used to talk to Nightscout. + * Creates the client with a platform engine, and whatever logging that engine supports. * - * The engine is **OkHttp**, so this reuses the HTTP stack the app already ships rather than adding a - * second one. When the module builds for iOS the engine becomes Darwin and nothing else here - * changes - that is the reason for moving off Retrofit at all. + * The engine is the one part of the HTTP stack that cannot be shared: JVM and Android use OkHttp + * (which is what the app already ships, so nothing new is added), and Apple targets use Darwin. + * Request logging is engine specific too, which is why it lives here rather than in the common + * configuration below. + */ +internal expect fun nsHttpClient( + logging: Boolean, + logger: (String) -> Unit, + configure: HttpClientConfig<*>.() -> Unit +): HttpClient + +/** + * Builds the Ktor client used to talk to Nightscout. * * Two settings are load bearing and must not be "improved": * @@ -54,7 +64,7 @@ internal object NsKtorClient { return Stack(NightscoutApi(main, baseUrl), main, refresh) } - fun build(logging: Boolean, logger: (String) -> Unit): HttpClient = HttpClient(OkHttp) { + fun build(logging: Boolean, logger: (String) -> Unit): HttpClient = nsHttpClient(logging, logger) { expectSuccess = false install(ContentNegotiation) { @@ -65,18 +75,6 @@ internal object NsKtorClient { socketTimeoutMillis = SOCKET_TIMEOUT connectTimeoutMillis = CONNECT_TIMEOUT } - - if (logging) { - engine { - addInterceptor { chain -> - val request = chain.request() - logger("--> ${request.method} ${request.url}") - val response = chain.proceed(request) - logger("<-- ${response.code} ${request.url}") - response - } - } - } } /** Matches the old OkHttp read timeout. */ diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/NsUrl.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/Status.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/Status.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/networking/Status.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/networking/Status.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/LastModified.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteAuthResponse.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt similarity index 95% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt index 0636531f1f87..cc110c3d4847 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteDeviceStatus.kt @@ -1,8 +1,8 @@ package app.aaps.core.nssdk.remotemodel -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject /** * DeviceStatus coming from uploader or AAPS @@ -55,8 +55,8 @@ internal data class RemoteDeviceStatus( @Serializable data class OpenAps( @SerialName("suggested") val suggested: JsonObject? = null, - @SerialName("enacted") val enacted: JsonObject? = null, - @SerialName("iob") val iob: JsonObject? = null + @SerialName("enacted") val enacted: JsonObject? = null, + @SerialName("iob") val iob: JsonObject? = null ) @Serializable diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteEntry.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteFood.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteICfg.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteStatusResponse.kt diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt similarity index 81% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt index 3e7df06ee729..b0cb3febded7 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/RemoteTreatment.kt @@ -21,21 +21,29 @@ import kotlinx.serialization.Serializable * */ @Serializable internal data class RemoteTreatment( - @SerialName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. - @SerialName("date") var date: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + @SerialName("identifier") + val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. + @SerialName("date") + var date: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') @SerialName("mills") val mills: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix - @SerialName("timestamp") val timestamp: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + @SerialName("timestamp") + val timestamp: Long? = null, // integer($int64) or string required timestamp when the record or event occurred, you can choose from three input formats Unix epoch in milliseconds (1525383610088), Unix epoch in seconds (1525383610), ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') @SerialName("created_at") val created_at: String? = null, // integer($int64) or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... - @SerialName("utcOffset") var utcOffset: Long? = null, // integer Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the date field. - @SerialName("app") var app : String? = null, // Application or system in which the record was entered by human or device for the first time. + @SerialName("utcOffset") + var utcOffset: Long? = null, // integer Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the date field. + @SerialName("app") var app: String? = null, // Application or system in which the record was entered by human or device for the first time. @SerialName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe). - @SerialName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + @SerialName("srvCreated") + val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. @SerialName("subject") val subject: String? = null, // string Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. - @SerialName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + @SerialName("srvModified") + val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). @SerialName("modifiedBy") val modifiedBy: String? = null, // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. - @SerialName("isValid") val isValid: Boolean? = null, // boolean A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) + @SerialName("isValid") + val isValid: Boolean? = null, // boolean A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value) @SerialName("isReadOnly") val isReadOnly: Boolean? = null, // boolean A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted. - @SerialName("eventType") val eventType: EventType? = null, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" + @SerialName("eventType") + val eventType: EventType? = null, // string "BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard" @SerialName("glucose") val glucose: Double? = null, // double Current glucose @SerialName("glucoseType") val glucoseType: String? = null, // string example: "Sensor", "Finger", "Manual" @SerialName("units") val units: String? = null, // string The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. @@ -80,7 +88,8 @@ internal data class RemoteTreatment( @SerialName("originalEnd") val originalEnd: Long? = null, // long "Effective Profile Switch" @SerialName("icfg") val iCfg: RemoteICfg? = null, // long "Effective Profile Switch" - @SerialName("bolusCalculatorResult") val bolusCalculatorResult: String? = null, // string "Bolus Wizard" json toString ex "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", + @SerialName("bolusCalculatorResult") + val bolusCalculatorResult: String? = null, // string "Bolus Wizard" json toString ex "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", @SerialName("type") val type: String? = null, // string "Meal Bolus", "Correction Bolus", "Combo Bolus", "Temp Basal" type of bolus "NORMAL", "SMB", "FAKE_EXTENDED" @SerialName("isSMB") val isSMB: Boolean? = null, // boolean "Meal Bolus", "Correction Bolus", "Combo Bolus" @SerialName("enteredinsulin") val enteredinsulin: Double? = null, // number... "Combo Bolus" insulin is missing only enteredinsulin field found diff --git a/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json new file mode 100644 index 000000000000..0ffff23e4df7 --- /dev/null +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json @@ -0,0 +1,489 @@ +// Entry +{ + "device": "xDrip-Follower", + "date": 1549414398005, + "dateString": "2019-02-06T01:53:18.005+0100", + "sgv": 98, + "delta": -1.132, + "direction": "Flat", + "type": "sgv", + "filtered": 90336, + "unfiltered": 89712, + "rssi": 100, + "noise": 1, + "sysTime": "2019-02-06T01:53:18.005+0100", + "identifier": "5c5a3007e0196f4d3d9aeafc", + "srvModified": 1549414398005, + "srvCreated": 1549414398005 +}, + +// G6 AAPS +{ +"_id": "60bace9f51e8150004f0973a", +"device": "AndroidAPS-DexcomG6", +"date": 1622855221000, +"dateString": "2021-06-05T01:07:01.000Z", +"isValid": true, +"sgv": 76, +"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp +"type": "sgv", +"created_at": "2021-06-05T01:08:47.234Z" +}, +// G6 DEXCOM APP Share +{ +"_id": "60cd4e7d5bcdeb30e43a248d", +"sgv": 90, +"date": 1624067551000, +"dateString": "2021-06-19T01:52:31.000Z", +"trend": 4, // 7 , 6 , 5 , 4 , 3 , 2 , 1 +"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp +"device": "share2", +"type": "sgv", +"utcOffset": 0, +"sysTime": "2021-06-19T01:52:31.000Z" +}, +// FSL1 xDrip +{ +"device": "AndroidAPS", +"date": 1588557121000, +"dateString": "2020-05-04T01:52:01Z", +"sgv": 76, +"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp +"type": "sgv", +"systime": "2020-05-04T01:52:01Z", +"utcOffset": 120 +}, +// LimiTTer xDrip +{ +"_id": "5ed06c9a0ea4dcb70fac6cc7", +"device": "xDrip-LimiTTer", +"date": 1590717591357, +"dateString": "2020-05-29T01:59:51.357Z", +"sgv": 114, +"delta": -2.942, +"direction": "Flat", +"type": "sgv", +"filtered": 127411.75515, +"unfiltered": 127411.75515, +"rssi": 100, +"noise": 1, +"sysTime": "2020-05-29T01:59:51.357Z", +"utcOffset": 120 +}, + +// API v3 requests for treatments +{ +"eventType": "BG Check", +"created_at": 1616966443000, +"units": "mg/dl", +"glucose": 57, +"NSCLIENT_ID": "1616966443000", +"identifier": "6060f32e9b9c5900045c858b", +"srvModified": 1616966443000, +"srvCreated": 1616966443000 +}, +{ +"eventType": "BG Check", +"created_at": 1617365936000, +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"notes": "Coucou", +"glucose": 94, +"glucoseType": "Finger", +"identifier": "606727c058f71500041e1ed3", +"srvModified": 1617365936000, +"srvCreated": 1617365936000 +}, +{ +"eventType": "Meal Bolus", +"carbs": 45, +"created_at": "2021-07-13T18:19:43.325Z", +"isValid": true, +"date": 1626200383325, +"identifier": "60ede2a4c574da0004a3869c", +"srvModified": 1626200383325, +"srvCreated": 1626200383325 +}, +{ +"eventType": "Meal Bolus", +"insulin": 8.1, +"created_at": "2021-07-13T11:25:12.664Z", +"date": 1626175512664, +"type": "NORMAL", +"isValid": true, +"isSMB": false, +"pumpId": 4102, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33013206", +"identifier": "60ed782dc574da0004a38595", +"srvModified": 1626175512664, +"srvCreated": 1626175512664 +}, +{ +"eventType": "Correction Bolus", +"insulin": 0.25, +"created_at": "2021-07-13T20:44:14.441Z", +"date": 1626209054441, +"type": "SMB", +"isValid": true, +"isSMB": true, +"pumpId": 4148, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33013206", +"identifier": "60edfb34c574da0004a386d4", +"srvModified": 1626209054441, +"srvCreated": 1626209054441 +},{ +"eventType": "Carb Correction", +"carbs": 5, +"created_at": "2021-06-17T09:00:34.000Z", +"isValid": true, +"date": 1623920434000, +"identifier": "60cb2f351a94d4000483b692", +"srvModified": 1623920434000, +"srvCreated": 1623920434000 +}, +{ +"created_at": "2021-05-28T19:46:43.851Z", +"enteredBy": "openaps://AndroidAPS", +"eventType": "Combo Bolus", +"duration": 5, +"splitNow": 0, +"splitExt": 100, +"enteredinsulin": 0.7890262726962469, +"relative": 8.893749414356174, +"isValid": true, +"isEmulatingTempBasal": false, +"pumpId": 4, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33010032", +"identifier": "60b148b419cf4300040b0195", +"srvModified": 1622231203851, +"srvCreated": 1622231203851 +}, +{ +"eventType": "Announcement", +"created_at": 1617350431592, +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"notes": "5g de glucides requis dans 40 min.", +"isAnnouncement": true, +"identifier": "6066cf2508a6ed0004b4ed44", +"srvModified": 1617350431592, +"srvCreated": 1617350431592 +}, +{ +"eventType": "Note", +"created_at": 1617023462485, +"units": "mg/dl", +"notes": "AndroidAPS started - Logicom Le Hola FR", +"identifier": "6061d20b17619800047216b2", +"srvModified": 1617023462485, +"srvCreated": 1617023462485 +}, +{ +"eventType": "Exercise", +"created_at": 1617373066000, +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"duration": 20, +"notes": "ten tab", +"identifier": "606727a658f71500041e1ed2", +"srvModified": 1617373066000, +"srvCreated": 1617373066000 +}, +{ +"eventType": "Exercise", +"isValid": true, +"created_at": "2021-07-09T18:15:22.000Z", +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"duration": 105, +"notes": "🏓", +"identifier": "60e8b223b98ea2000472cbb3", +"srvModified": 1625854522000, +"srvCreated": 1625854522000 +}, +{ +"eventType": "Site Change", +"created_at": 1616312250000, +"units": "mg/dl", +"notes": "", +"NSCLIENT_ID": "1616312250000", +"identifier": "6056f7c1bc2dc60004e75499", +"srvModified": 1616312250000, +"srvCreated": 1616312250000 +}, +{ +"eventType": "Sensor Change", +"created_at": 1617373059000, +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"identifier": "6067278d58f71500041e1ed1", +"srvModified": 1617373059000, +"srvCreated": 1617373059000 +}, +{ +"enteredBy": "AndroidAPS-DexcomG6", +"created_at": 1617799461000, +"eventType": "Sensor Change", +"NSCLIENT_ID": "1617961262771", +"identifier": "60702190403172000451e5dc", +"srvModified": 1617799461000, +"srvCreated": 1617799461000 +}, +{ +"eventType": "Pump Battery Change", +"created_at": 1616517575000, +"enteredBy": "AndroidAPS", +"units": "mg/dl", +"notes": "à peu près...", +"NSCLIENT_ID": "1616517575000", +"identifier": "605cbce3f9ed3b0004694ee8", +"srvModified": 1616517575000, +"srvCreated": 1616517575000 +}, +{ +"created_at": 1617576811000, +"eventType": "Pump Battery Change", +"NSCLIENT_ID": "1617577097394", +"glucoseType": "Manual", +"isValid": true, +"units": "mg/dl", +"identifier": "606a448d7c31f00004bb47ac", +"srvModified": 1617576811000, +"srvCreated": 1617576811000 +}, +{ +"eventType": "Insulin Change", +"created_at": 1616342559000, +"units": "mg/dl", +"notes": "Ajout manuel pour UE", +"NSCLIENT_ID": "1616342559000", +"identifier": "60576e6b5a34f900043e25f6", +"srvModified": 1616342559000, +"srvCreated": 1616342559000 +}, +{ +"created_at": "2021-07-13T20:44:12.891Z", +"enteredBy": "openaps://AndroidAPS", +"eventType": "Temp Basal", +"isValid": true, +"duration": 60, +"rate": 0, +"type": "NORMAL", +"absolute": 0, +"pumpId": 284835, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33013206", +"identifier": "60edfb34c574da0004a386d3", +"srvModified": 1626209052891, +"srvCreated": 1626209052891 +}, +{ +"created_at": "2021-07-13T20:40:29.896Z", +"enteredBy": "openaps://AndroidAPS", +"eventType": "Temp Basal", +"isValid": true, +"duration": 3, +"rate": 2.4391549295774646, +"type": "FAKE_EXTENDED", +"absolute": 2.4391549295774646, +"pumpId": 4147, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33013206", +"extendedEmulated": { +"created_at": "2021-07-13T20:40:29.896Z", +"enteredBy": "openaps://AndroidAPS", +"eventType": "Combo Bolus", +"duration": 3, +"splitNow": 0, +"splitExt": 100, +"enteredinsulin": 0.11, +"relative": 1.8591549295774648, +"isValid": true, +"isEmulatingTempBasal": true, +"pumpId": 4147, +"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", +"pumpSerial": "33013206" +}, +"identifier": "60edfa51c574da0004a386d0", +"srvModified": 1626208829896, +"srvCreated": 1626208829896 +}, +{ +"eventType": "OpenAPS Offline", +"created_at": 1616391934628, +"enteredBy": "openaps://AndroidAPS", +"units": "mg/dl", +"duration": 15, +"NSCLIENT_ID": "1616391934628", +"identifier": "60582f005a34f900043e2845", +"srvModified": 1616391934628, +"srvCreated": 1616391934628 +}, +{ +"created_at": "2021-06-26T13:36:47.000Z", +"enteredBy": "openaps://AndroidAPS", +"isValid": true, +"eventType": "Profile Switch", +"duration": 0, +"profile": "Tuned 13/01 90%Lyum", +"profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":45},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":45},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":46},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":49},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":52},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":55},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":54},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":51},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":49},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":49}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":4.3},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":5.5},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":5}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.7},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.78},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.85},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.75},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.61},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.69},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.64},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.68},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.65},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.64},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.65},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.67},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.74},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.76},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.76},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.77},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.68},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.72},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.65},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.7},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.62},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.62},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.58},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.55}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", +"timeshift": 0, +"percentage": 100, +"identifier": "60d72d80aec46a0004f95163", +"srvModified": 1624714607000, +"srvCreated": 1624714607000 +}, +{ +"created_at": "2021-06-13T07:20:33.000Z", +"enteredBy": "openaps://AndroidAPS", +"isValid": true, +"eventType": "Profile Switch", +"duration": 150, +"profile": "Tuned 13/01 90%Lyum(75%)", +"profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":60},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":60},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":61.33333333333333},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":65.33333333333333},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":69.33333333333333},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":73.33333333333333},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":72},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":68},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":65.33333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":65.33333333333333}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":5.7333333333333325},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":7.333333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":6.666666666666666}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.5249999999999999},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.585},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.6375},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.5625},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.4575},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.5175},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.48},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.51},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.48750000000000004},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.48},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.48750000000000004},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.5025000000000001},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.5549999999999999},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.5700000000000001},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.5700000000000001},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.5775},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.51},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.54},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.48750000000000004},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.5249999999999999},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.46499999999999997},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.46499999999999997},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.43499999999999994},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.41250000000000003}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", +"timeshift": 0, +"percentage": 100, +"identifier": "60c5b1f41b3715000420af27", +"srvModified": 1623568833000, +"srvCreated": 1623568833000 +}, +{ +"eventType": "Temporary Target", +"duration": 60, +"isValid": true, +"created_at": "2021-07-10T05:04:11.566Z", +"enteredBy": "AndroidAPS", +"reason": "Automation", +"targetBottom": 110, +"targetTop": 110, +"units": "mg/dl", +"identifier": "60e92a644fc2eb00045ece1b", +"srvModified": 1625893451566, +"srvCreated": 1625893451566 +}, +{ +"eventType": "Temporary Target", +"duration": 120, +"isValid": true, +"created_at": "2021-07-09T20:30:21.627Z", +"enteredBy": "AndroidAPS", +"reason": "Hypo", +"targetBottom": 140, +"targetTop": 140, +"units": "mg/dl", +"identifier": "60e8b1f2b98ea2000472cbb1", +"srvModified": 1625862621627, +"srvCreated": 1625862621627 +}, +{ +"eventType": "Bolus Wizard", +"created_at": "2021-07-13T18:59:43.325Z", +"isValid": true, +"bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", +"date": 1626202783325, +"glucose": 134, +"units": "mg/dl", +"notes": "", +"identifier": "60ede2a4c574da0004a3869d", +"srvModified": 1626202783325, +"srvCreated": 1626202783325 +}, +DEVICE STATUS with configuration +--------------------------------- +{ +"_id": "635abf2069a34517e83768cd", +"created_at": "2022-10-27T17:25:49.730Z", +"device": "openaps://samsung SM-G970F", +"pump": { +"battery": { +"percent": 100 +}, +"status": { +"status": "normal", +"timestamp": "2022-10-27T17:16:11.504Z" +}, +"extended": { +"Version": "3.1.0.3-dev-c-nscv3-8da78d7351-2022.10.25-19:56", +"LastBolus": "10/27/22 18:40", +"LastBolusAmount": 0.35, +"TempBasalAbsoluteRate": 0, +"TempBasalStart": "10/27/22 18:50", +"TempBasalRemaining": 24, +"BaseBasalRate": 1, +"ActiveProfile": "LocalProfile1" +}, +"reservoir": 191, +"clock": "2022-10-27T17:25:49.759Z" +}, +"openaps": { +"suggested": { +"temp": "absolute", +"bg": 72, +"tick": -6, +"eventualBG": 4, +"snoozeBG": 4, +"predBGs": { +"IOB": [72, 61, 51, 42, 39, 39, 39, 39, 39, 39, 39, 39, 39] +}, +"COB": 0, +"IOB": 0.052, +"reason": "COB: 0, Dev: -66, BGI: -0.88, ISF: 2.0, Target: 6.0; BG 4.0<4.4, but 25m left and 0 ~ req 0U/hr: no action required", +"timestamp": "2022-10-27T17:25:49.726Z" +}, +"iob": { +"iob": 0.052, +"basaliob": 0.052, +"activity": 0.0049, +"time": "2022-10-27T17:25:49.726Z" +} +}, +"uploaderBattery": 100, +"configuration": { +"insulin": 5, +"insulinConfiguration": {}, +"sensitivity": 2, +"sensitivityConfiguration": { +"openapsama_min_5m_carbimpact": 10, +"absorption_cutoff": 4, +"autosens_max": 1.2, +"autosens_min": 0.7 +}, +"overviewConfiguration": { +"units": "mmol", +"eatingsoon_duration": 0, +"eatingsoon_target": 0, +"activity_duration": 0, +"activity_target": 0, +"hypo_duration": 0, +"hypo_target": 0, +"low_mark": 4, +"high_mark": 0, +"statuslights_cage_warning": 48, +"statuslights_cage_critical": 72, +"statuslights_iage_warning": 72, +"statuslights_iage_critical": 144, +"statuslights_sage_warning": 216, +"statuslights_sage_critical": 240, +"statuslights_sbat_warning": 25, +"statuslights_sbat_critical": 5, +"statuslights_bage_warning": 216, +"statuslights_bage_critical": 240, +"statuslights_res_warning": 80, +"statuslights_res_critical": 10, +"statuslights_bat_warning": 25, +"statuslights_bat_critical": 5, +"boluswizard_percentage": 60 +}, +"safetyConfiguration": { +"age": "teenage", +"treatmentssafety_maxbolus": 4, +"treatmentssafety_maxcarbs": 60 +}, +"pump": "DanaR", +"version": "3.1.0.3-dev-c-nscv3" +}, +"mills": 1666891549730 +} \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt similarity index 89% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt rename to core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt index d94a92892052..8c97a745f71e 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/CoroutineUtils.kt @@ -7,7 +7,7 @@ import kotlin.reflect.KClass internal suspend fun retry( numberOfRetries: Int, delayBetweenRetries: Long, - excludedExceptions: List>, + excludedExceptions: List>, block: suspend () -> T ): T { repeat(numberOfRetries) { diff --git a/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.kt new file mode 100644 index 000000000000..7bfc131bf5de --- /dev/null +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.kt @@ -0,0 +1,12 @@ +package app.aaps.core.nssdk.utils + +import kotlinx.coroutines.CoroutineDispatcher + +/** + * The dispatcher network calls run on. + * + * `Dispatchers.IO` is not part of the common coroutines API - it exists on JVM and on Native, but not + * as one shared declaration - so it is selected per platform here. The JVM actual is the same + * `Dispatchers.IO` this client has always used, so nothing about threading changes on Android. + */ +internal expect val nsIoDispatcher: CoroutineDispatcher diff --git a/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt new file mode 100644 index 000000000000..85797943dbc0 --- /dev/null +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt @@ -0,0 +1,5 @@ +package app.aaps.core.nssdk.utils + +// @JvmSynthetic was here to hide this helper from Java callers. It is a JVM-only annotation, and the +// function is `internal` anyway, so it is dropped rather than made platform specific. +internal fun List?.toNotNull(): List = this?.filterNotNull() ?: listOf() diff --git a/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.jvm.kt b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.jvm.kt new file mode 100644 index 000000000000..e6eb1341490f --- /dev/null +++ b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.jvm.kt @@ -0,0 +1,33 @@ +package app.aaps.core.nssdk.networking + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.okhttp.OkHttp + +/** + * JVM and Android engine: **OkHttp**. + * + * The app already ships OkHttp for other things, so using it here adds Ktor's own layers rather than + * a second HTTP stack - which was the point of choosing this engine over CIO. + * + * The logging interceptor is OkHttp's, which is why request logging lives in the actual rather than + * in the shared configuration. An Apple target would install Darwin here and log differently. + */ +internal actual fun nsHttpClient( + logging: Boolean, + logger: (String) -> Unit, + configure: HttpClientConfig<*>.() -> Unit +): HttpClient = HttpClient(OkHttp) { + configure() + if (logging) { + engine { + addInterceptor { chain -> + val request = chain.request() + logger("--> ${request.method} ${request.url}") + val response = chain.proceed(request) + logger("<-- ${response.code} ${request.url}") + response + } + } + } +} diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt similarity index 97% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt rename to core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt index e858f95cdac4..fa79ac4fe99f 100644 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt +++ b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/ClientControlCrypto.kt @@ -3,6 +3,8 @@ package app.aaps.core.nssdk.utils import app.aaps.core.nssdk.localmodel.clientcontrol.AckEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.SignedEnvelope +import app.aaps.core.nssdk.utils.ClientControlCrypto.bytesToHex +import app.aaps.core.nssdk.utils.ClientControlCrypto.hexToBytes import app.aaps.core.nssdk.utils.ClientControlCrypto.timestampWithinSkew import java.security.MessageDigest import java.security.SecureRandom diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCrypto.kt b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCrypto.kt similarity index 100% rename from core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCrypto.kt rename to core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCrypto.kt diff --git a/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.jvm.kt b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.jvm.kt new file mode 100644 index 000000000000..56234b09194e --- /dev/null +++ b/core/nssdk/src/jvmMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.jvm.kt @@ -0,0 +1,7 @@ +package app.aaps.core.nssdk.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +/** Unchanged from before the multiplatform split. */ +internal actual val nsIoDispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/NsSdkWireFormatTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/exceptions/NightscoutExceptionTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapperTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapperTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapperTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/ApiPermissionMapperTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/CreatedAtParsingTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/DeviceStatusMapperTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/FoodAndEntryToleranceTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/KotlinxCoercionSpikeTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/MbgMapperTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/MbgMapperTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/MbgMapperTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/MbgMapperTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/RealNightscoutTreatmentTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapperTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapperTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapperTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/StatusResponseMapperTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt index 8e63c4ec1f15..eadbb5f3f872 100644 --- a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt +++ b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/mapper/WireTypeCoercionTest.kt @@ -2,10 +2,10 @@ package app.aaps.core.nssdk.mapper import app.aaps.core.nssdk.localmodel.treatment.NSBolus import app.aaps.core.nssdk.localmodel.treatment.NSCarbs -import com.google.common.truth.Truth.assertThat import app.aaps.core.nssdk.nsSdkJson -import kotlinx.serialization.SerializationException import app.aaps.core.nssdk.remotemodel.RemoteTreatment +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.SerializationException import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NightscoutApiUrlTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkAuthContractTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkErrorBodyContractTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkResponseContractTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkStatusContractTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/networking/NsSdkUrlContractTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlCryptoVectorsTest.kt diff --git a/core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCryptoTest.kt b/core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCryptoTest.kt similarity index 100% rename from core/nssdk/src/test/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCryptoTest.kt rename to core/nssdk/src/jvmTest/kotlin/app/aaps/core/nssdk/utils/ClientControlPairingCryptoTest.kt diff --git a/core/nssdk/src/main/AndroidManifest.xml b/core/nssdk/src/main/AndroidManifest.xml deleted file mode 100644 index a8800291f3c2..000000000000 --- a/core/nssdk/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/examples.json b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/examples.json deleted file mode 100644 index 87b19cad8b79..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/remotemodel/examples.json +++ /dev/null @@ -1,490 +0,0 @@ -// Entry -{ - "device": "xDrip-Follower", - "date": 1549414398005, - "dateString": "2019-02-06T01:53:18.005+0100", - "sgv": 98, - "delta": -1.132, - "direction": "Flat", - "type": "sgv", - "filtered": 90336, - "unfiltered": 89712, - "rssi": 100, - "noise": 1, - "sysTime": "2019-02-06T01:53:18.005+0100", - "identifier": "5c5a3007e0196f4d3d9aeafc", - "srvModified": 1549414398005, - "srvCreated": 1549414398005 -}, - -// G6 AAPS -{ - "_id": "60bace9f51e8150004f0973a", - "device": "AndroidAPS-DexcomG6", - "date": 1622855221000, - "dateString": "2021-06-05T01:07:01.000Z", - "isValid": true, - "sgv": 76, - "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp - "type": "sgv", - "created_at": "2021-06-05T01:08:47.234Z" -}, -// G6 DEXCOM APP Share -{ - "_id": "60cd4e7d5bcdeb30e43a248d", - "sgv": 90, - "date": 1624067551000, - "dateString": "2021-06-19T01:52:31.000Z", - "trend": 4, // 7 , 6 , 5 , 4 , 3 , 2 , 1 - "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp - "device": "share2", - "type": "sgv", - "utcOffset": 0, - "sysTime": "2021-06-19T01:52:31.000Z" -}, -// FSL1 xDrip -{ - "device": "AndroidAPS", - "date": 1588557121000, - "dateString": "2020-05-04T01:52:01Z", - "sgv": 76, - "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp - "type": "sgv", - "systime": "2020-05-04T01:52:01Z", - "utcOffset": 120 -}, -// LimiTTer xDrip -{ - "_id": "5ed06c9a0ea4dcb70fac6cc7", - "device": "xDrip-LimiTTer", - "date": 1590717591357, - "dateString": "2020-05-29T01:59:51.357Z", - "sgv": 114, - "delta": -2.942, - "direction": "Flat", - "type": "sgv", - "filtered": 127411.75515, - "unfiltered": 127411.75515, - "rssi": 100, - "noise": 1, - "sysTime": "2020-05-29T01:59:51.357Z", - "utcOffset": 120 -}, - -// API v3 requests for treatments -{ - "eventType": "BG Check", - "created_at": 1616966443000, - "units": "mg/dl", - "glucose": 57, - "NSCLIENT_ID": "1616966443000", - "identifier": "6060f32e9b9c5900045c858b", - "srvModified": 1616966443000, - "srvCreated": 1616966443000 -}, -{ - "eventType": "BG Check", - "created_at": 1617365936000, - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "notes": "Coucou", - "glucose": 94, - "glucoseType": "Finger", - "identifier": "606727c058f71500041e1ed3", - "srvModified": 1617365936000, - "srvCreated": 1617365936000 -}, -{ - "eventType": "Meal Bolus", - "carbs": 45, - "created_at": "2021-07-13T18:19:43.325Z", - "isValid": true, - "date": 1626200383325, - "identifier": "60ede2a4c574da0004a3869c", - "srvModified": 1626200383325, - "srvCreated": 1626200383325 -}, -{ - "eventType": "Meal Bolus", - "insulin": 8.1, - "created_at": "2021-07-13T11:25:12.664Z", - "date": 1626175512664, - "type": "NORMAL", - "isValid": true, - "isSMB": false, - "pumpId": 4102, - "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", - "pumpSerial": "33013206", - "identifier": "60ed782dc574da0004a38595", - "srvModified": 1626175512664, - "srvCreated": 1626175512664 -}, -{ - "eventType": "Correction Bolus", - "insulin": 0.25, - "created_at": "2021-07-13T20:44:14.441Z", - "date": 1626209054441, - "type": "SMB", - "isValid": true, - "isSMB": true, - "pumpId": 4148, - "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", - "pumpSerial": "33013206", - "identifier": "60edfb34c574da0004a386d4", - "srvModified": 1626209054441, - "srvCreated": 1626209054441 -},{ - "eventType": "Carb Correction", - "carbs": 5, - "created_at": "2021-06-17T09:00:34.000Z", - "isValid": true, - "date": 1623920434000, - "identifier": "60cb2f351a94d4000483b692", - "srvModified": 1623920434000, - "srvCreated": 1623920434000 -}, -{ -"created_at": "2021-05-28T19:46:43.851Z", -"enteredBy": "openaps://AndroidAPS", -"eventType": "Combo Bolus", -"duration": 5, -"splitNow": 0, -"splitExt": 100, -"enteredinsulin": 0.7890262726962469, -"relative": 8.893749414356174, -"isValid": true, -"isEmulatingTempBasal": false, -"pumpId": 4, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33010032", -"identifier": "60b148b419cf4300040b0195", -"srvModified": 1622231203851, -"srvCreated": 1622231203851 -}, -{ - "eventType": "Announcement", - "created_at": 1617350431592, - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "notes": "5g de glucides requis dans 40 min.", - "isAnnouncement": true, - "identifier": "6066cf2508a6ed0004b4ed44", - "srvModified": 1617350431592, - "srvCreated": 1617350431592 -}, -{ - "eventType": "Note", - "created_at": 1617023462485, - "units": "mg/dl", - "notes": "AndroidAPS started - Logicom Le Hola FR", - "identifier": "6061d20b17619800047216b2", - "srvModified": 1617023462485, - "srvCreated": 1617023462485 -}, -{ - "eventType": "Exercise", - "created_at": 1617373066000, - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "duration": 20, - "notes": "ten tab", - "identifier": "606727a658f71500041e1ed2", - "srvModified": 1617373066000, - "srvCreated": 1617373066000 -}, -{ - "eventType": "Exercise", - "isValid": true, - "created_at": "2021-07-09T18:15:22.000Z", - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "duration": 105, - "notes": "🏓", - "identifier": "60e8b223b98ea2000472cbb3", - "srvModified": 1625854522000, - "srvCreated": 1625854522000 -}, -{ - "eventType": "Site Change", - "created_at": 1616312250000, - "units": "mg/dl", - "notes": "", - "NSCLIENT_ID": "1616312250000", - "identifier": "6056f7c1bc2dc60004e75499", - "srvModified": 1616312250000, - "srvCreated": 1616312250000 -}, -{ - "eventType": "Sensor Change", - "created_at": 1617373059000, - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "identifier": "6067278d58f71500041e1ed1", - "srvModified": 1617373059000, - "srvCreated": 1617373059000 -}, -{ - "enteredBy": "AndroidAPS-DexcomG6", - "created_at": 1617799461000, - "eventType": "Sensor Change", - "NSCLIENT_ID": "1617961262771", - "identifier": "60702190403172000451e5dc", - "srvModified": 1617799461000, - "srvCreated": 1617799461000 -}, -{ - "eventType": "Pump Battery Change", - "created_at": 1616517575000, - "enteredBy": "AndroidAPS", - "units": "mg/dl", - "notes": "à peu près...", - "NSCLIENT_ID": "1616517575000", - "identifier": "605cbce3f9ed3b0004694ee8", - "srvModified": 1616517575000, - "srvCreated": 1616517575000 -}, -{ - "created_at": 1617576811000, - "eventType": "Pump Battery Change", - "NSCLIENT_ID": "1617577097394", - "glucoseType": "Manual", - "isValid": true, - "units": "mg/dl", - "identifier": "606a448d7c31f00004bb47ac", - "srvModified": 1617576811000, - "srvCreated": 1617576811000 -}, -{ - "eventType": "Insulin Change", - "created_at": 1616342559000, - "units": "mg/dl", - "notes": "Ajout manuel pour UE", - "NSCLIENT_ID": "1616342559000", - "identifier": "60576e6b5a34f900043e25f6", - "srvModified": 1616342559000, - "srvCreated": 1616342559000 -}, -{ - "created_at": "2021-07-13T20:44:12.891Z", - "enteredBy": "openaps://AndroidAPS", - "eventType": "Temp Basal", - "isValid": true, - "duration": 60, - "rate": 0, - "type": "NORMAL", - "absolute": 0, - "pumpId": 284835, - "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", - "pumpSerial": "33013206", - "identifier": "60edfb34c574da0004a386d3", - "srvModified": 1626209052891, - "srvCreated": 1626209052891 -}, -{ - "created_at": "2021-07-13T20:40:29.896Z", - "enteredBy": "openaps://AndroidAPS", - "eventType": "Temp Basal", - "isValid": true, - "duration": 3, - "rate": 2.4391549295774646, - "type": "FAKE_EXTENDED", - "absolute": 2.4391549295774646, - "pumpId": 4147, - "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", - "pumpSerial": "33013206", - "extendedEmulated": { - "created_at": "2021-07-13T20:40:29.896Z", - "enteredBy": "openaps://AndroidAPS", - "eventType": "Combo Bolus", - "duration": 3, - "splitNow": 0, - "splitExt": 100, - "enteredinsulin": 0.11, - "relative": 1.8591549295774648, - "isValid": true, - "isEmulatingTempBasal": true, - "pumpId": 4147, - "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", - "pumpSerial": "33013206" - }, - "identifier": "60edfa51c574da0004a386d0", - "srvModified": 1626208829896, - "srvCreated": 1626208829896 -}, -{ - "eventType": "OpenAPS Offline", - "created_at": 1616391934628, - "enteredBy": "openaps://AndroidAPS", - "units": "mg/dl", - "duration": 15, - "NSCLIENT_ID": "1616391934628", - "identifier": "60582f005a34f900043e2845", - "srvModified": 1616391934628, - "srvCreated": 1616391934628 -}, -{ - "created_at": "2021-06-26T13:36:47.000Z", - "enteredBy": "openaps://AndroidAPS", - "isValid": true, - "eventType": "Profile Switch", - "duration": 0, - "profile": "Tuned 13/01 90%Lyum", - "profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":45},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":45},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":46},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":49},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":52},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":55},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":54},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":51},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":49},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":49}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":4.3},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":5.5},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":5}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.7},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.78},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.85},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.75},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.61},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.69},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.64},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.68},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.65},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.64},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.65},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.67},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.74},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.76},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.76},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.77},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.68},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.72},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.65},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.7},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.62},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.62},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.58},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.55}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", - "timeshift": 0, - "percentage": 100, - "identifier": "60d72d80aec46a0004f95163", - "srvModified": 1624714607000, - "srvCreated": 1624714607000 -}, -{ - "created_at": "2021-06-13T07:20:33.000Z", - "enteredBy": "openaps://AndroidAPS", - "isValid": true, - "eventType": "Profile Switch", - "duration": 150, - "profile": "Tuned 13/01 90%Lyum(75%)", - "profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":60},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":60},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":61.33333333333333},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":65.33333333333333},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":69.33333333333333},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":73.33333333333333},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":72},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":68},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":65.33333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":65.33333333333333}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":5.7333333333333325},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":7.333333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":6.666666666666666}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.5249999999999999},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.585},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.6375},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.5625},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.4575},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.5175},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.48},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.51},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.48750000000000004},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.48},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.48750000000000004},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.5025000000000001},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.5549999999999999},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.5700000000000001},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.5700000000000001},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.5775},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.51},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.54},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.48750000000000004},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.5249999999999999},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.46499999999999997},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.46499999999999997},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.43499999999999994},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.41250000000000003}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", - "timeshift": 0, - "percentage": 100, - "identifier": "60c5b1f41b3715000420af27", - "srvModified": 1623568833000, - "srvCreated": 1623568833000 -}, -{ - "eventType": "Temporary Target", - "duration": 60, - "isValid": true, - "created_at": "2021-07-10T05:04:11.566Z", - "enteredBy": "AndroidAPS", - "reason": "Automation", - "targetBottom": 110, - "targetTop": 110, - "units": "mg/dl", - "identifier": "60e92a644fc2eb00045ece1b", - "srvModified": 1625893451566, - "srvCreated": 1625893451566 -}, -{ - "eventType": "Temporary Target", - "duration": 120, - "isValid": true, - "created_at": "2021-07-09T20:30:21.627Z", - "enteredBy": "AndroidAPS", - "reason": "Hypo", - "targetBottom": 140, - "targetTop": 140, - "units": "mg/dl", - "identifier": "60e8b1f2b98ea2000472cbb1", - "srvModified": 1625862621627, - "srvCreated": 1625862621627 -}, -{ - "eventType": "Bolus Wizard", - "created_at": "2021-07-13T18:59:43.325Z", - "isValid": true, - "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", - "date": 1626202783325, - "glucose": 134, - "units": "mg/dl", - "notes": "", - "identifier": "60ede2a4c574da0004a3869d", - "srvModified": 1626202783325, - "srvCreated": 1626202783325 -}, - -DEVICE STATUS with configuration ---------------------------------- -{ - "_id": "635abf2069a34517e83768cd", - "created_at": "2022-10-27T17:25:49.730Z", - "device": "openaps://samsung SM-G970F", - "pump": { - "battery": { - "percent": 100 - }, - "status": { - "status": "normal", - "timestamp": "2022-10-27T17:16:11.504Z" - }, - "extended": { - "Version": "3.1.0.3-dev-c-nscv3-8da78d7351-2022.10.25-19:56", - "LastBolus": "10/27/22 18:40", - "LastBolusAmount": 0.35, - "TempBasalAbsoluteRate": 0, - "TempBasalStart": "10/27/22 18:50", - "TempBasalRemaining": 24, - "BaseBasalRate": 1, - "ActiveProfile": "LocalProfile1" - }, - "reservoir": 191, - "clock": "2022-10-27T17:25:49.759Z" - }, - "openaps": { - "suggested": { - "temp": "absolute", - "bg": 72, - "tick": -6, - "eventualBG": 4, - "snoozeBG": 4, - "predBGs": { - "IOB": [72, 61, 51, 42, 39, 39, 39, 39, 39, 39, 39, 39, 39] - }, - "COB": 0, - "IOB": 0.052, - "reason": "COB: 0, Dev: -66, BGI: -0.88, ISF: 2.0, Target: 6.0; BG 4.0<4.4, but 25m left and 0 ~ req 0U/hr: no action required", - "timestamp": "2022-10-27T17:25:49.726Z" - }, - "iob": { - "iob": 0.052, - "basaliob": 0.052, - "activity": 0.0049, - "time": "2022-10-27T17:25:49.726Z" - } - }, - "uploaderBattery": 100, - "configuration": { - "insulin": 5, - "insulinConfiguration": {}, - "sensitivity": 2, - "sensitivityConfiguration": { - "openapsama_min_5m_carbimpact": 10, - "absorption_cutoff": 4, - "autosens_max": 1.2, - "autosens_min": 0.7 - }, - "overviewConfiguration": { - "units": "mmol", - "eatingsoon_duration": 0, - "eatingsoon_target": 0, - "activity_duration": 0, - "activity_target": 0, - "hypo_duration": 0, - "hypo_target": 0, - "low_mark": 4, - "high_mark": 0, - "statuslights_cage_warning": 48, - "statuslights_cage_critical": 72, - "statuslights_iage_warning": 72, - "statuslights_iage_critical": 144, - "statuslights_sage_warning": 216, - "statuslights_sage_critical": 240, - "statuslights_sbat_warning": 25, - "statuslights_sbat_critical": 5, - "statuslights_bage_warning": 216, - "statuslights_bage_critical": 240, - "statuslights_res_warning": 80, - "statuslights_res_critical": 10, - "statuslights_bat_warning": 25, - "statuslights_bat_critical": 5, - "boluswizard_percentage": 60 - }, - "safetyConfiguration": { - "age": "teenage", - "treatmentssafety_maxbolus": 4, - "treatmentssafety_maxcarbs": 60 - }, - "pump": "DanaR", - "version": "3.1.0.3-dev-c-nscv3" - }, - "mills": 1666891549730 -} \ No newline at end of file diff --git a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt b/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt deleted file mode 100644 index ebef730e4405..000000000000 --- a/core/nssdk/src/main/kotlin/app/aaps/core/nssdk/utils/ListUtils.kt +++ /dev/null @@ -1,4 +0,0 @@ -package app.aaps.core.nssdk.utils - -@JvmSynthetic -internal fun List?.toNotNull(): List = this?.filterNotNull() ?: listOf() diff --git a/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.mingwX64.kt b/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.mingwX64.kt new file mode 100644 index 000000000000..bd47112c9732 --- /dev/null +++ b/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.mingwX64.kt @@ -0,0 +1,21 @@ +package app.aaps.core.nssdk.networking + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.cio.CIO + +/** + * Native engine: **CIO**, Ktor's own pure-Kotlin engine. + * + * This target is a compile-time proof that commonMain carries no JVM API - it is not a shipping + * platform. An Apple target would use Darwin here instead, which is the engine an iOS build wants. + * + * Request logging is engine specific and is not wired up for this target, because nothing runs it. + */ +internal actual fun nsHttpClient( + logging: Boolean, + logger: (String) -> Unit, + configure: HttpClientConfig<*>.() -> Unit +): HttpClient = HttpClient(CIO) { + configure() +} diff --git a/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.mingwX64.kt b/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.mingwX64.kt new file mode 100644 index 000000000000..0d205d214b61 --- /dev/null +++ b/core/nssdk/src/mingwX64Main/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.mingwX64.kt @@ -0,0 +1,12 @@ +package app.aaps.core.nssdk.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +/** + * Native has no separate IO pool, so blocking work shares the default dispatcher. + * + * This target exists to prove commonMain carries no JVM API; an Apple target would use + * `Dispatchers.IO` here, which Kotlin/Native does provide. + */ +internal actual val nsIoDispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 486eb5c20be3..c3bc6ce836cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -136,6 +136,7 @@ io-ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.re io-ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } io-ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } io-ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +io-ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } com-squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } com-squareup-retrofit2-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } From 307b1ed615e04d1409f824b1ec7594f51dfc20f5 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 7 Aug 2026 08:13:07 +0200 Subject: [PATCH 012/146] TextRef --- _docs/KMP_IOS_FEASIBILITY.md | 553 ++++++++++++------ core/interfaces/build.gradle.kts | 2 +- .../interfaces/resources/ResourceHelper.kt | 23 + .../kotlin/app/aaps/core/keys/BooleanKey.kt | 9 +- .../kotlin/app/aaps/core/keys/DoubleKey.kt | 59 +- .../main/kotlin/app/aaps/core/keys/IntKey.kt | 11 +- .../kotlin/app/aaps/core/keys/IntentKey.kt | 15 +- .../kotlin/app/aaps/core/keys/StringKey.kt | 11 +- .../app/aaps/core/keys/UnitDoubleKey.kt | 11 +- .../core/keys/interfaces/PreferenceKey.kt | 13 +- .../app/aaps/core/keys/interfaces/TextRef.kt | 52 ++ .../aaps/core/ui/compose/TextRefResource.kt | 27 + .../preference/AdaptiveDoublePreference.kt | 21 +- .../preference/AdaptiveIntPreference.kt | 21 +- .../preference/AdaptiveIntentPreference.kt | 67 +-- .../preference/AdaptiveListPreference.kt | 32 +- .../preference/AdaptivePasswordPreference.kt | 15 +- .../preference/AdaptivePreferenceItem.kt | 14 +- .../preference/AdaptivePreferenceList.kt | 1 - .../preference/AdaptiveStringPreference.kt | 31 +- .../preference/AdaptiveSwitchPreference.kt | 25 +- .../AdaptiveUnitDoublePreference.kt | 21 +- .../ClickablePreferenceCategoryHeader.kt | 10 +- .../CollapsibleCardSectionContent.kt | 7 +- .../CollapsibleCardSectionContentPreviews.kt | 3 +- .../preference/InlinePreferenceItems.kt | 25 +- .../preference/PreferenceContentExtensions.kt | 4 +- .../preference/PreferenceSheetContent.kt | 4 +- .../ui/compose/preference/PreferenceState.kt | 2 +- .../preference/PreferenceSubScreenDef.kt | 19 +- .../core/ui/compose/preference/SyncBadge.kt | 6 +- .../app/aaps/core/ui/search/SearchableItem.kt | 34 +- .../MorePreferenceComponentsTest.kt | 7 +- .../utils/HardLimitsImplTest.kt | 2 +- .../app/aaps/plugins/aps/keys/ApsIntentKey.kt | 9 +- .../setupwizard/elements/SWEditIntNumber.kt | 3 +- .../setupwizard/elements/SWEditNumber.kt | 3 +- .../elements/SWEditNumberWithUnits.kt | 3 +- .../setupwizard/elements/SWEditString.kt | 3 +- .../setupwizard/elements/SWEditUrl.kt | 3 +- .../setupwizard/elements/SWRadioButton.kt | 3 +- .../source/instara/InstaraBooleanKey.kt | 11 +- .../sync/garmin/keys/GarminBooleanKey.kt | 6 +- .../plugins/sync/garmin/keys/GarminIntKey.kt | 6 +- .../sync/garmin/keys/GarminStringKey.kt | 6 +- .../sync/smsCommunicator/keys/SmsIntentKey.kt | 9 +- .../sync/tidepool/keys/TidepoolBooleanKey.kt | 11 +- .../plugins/sync/xdrip/keys/XdripIntentKey.kt | 9 +- .../pump/combov2/keys/ComboBooleanKey.kt | 6 +- .../pump/combov2/keys/ComboIntKey.kt | 6 +- .../app/aaps/pump/dana/keys/DanaBooleanKey.kt | 9 +- .../app/aaps/pump/dana/keys/DanaIntKey.kt | 6 +- .../app/aaps/pump/dana/keys/DanaIntentKey.kt | 8 +- .../pump/diaconn/keys/DiaconnBooleanKey.kt | 9 +- .../aaps/pump/diaconn/keys/DiaconnIntKey.kt | 6 +- .../pump/diaconn/keys/DiaconnIntentKey.kt | 8 +- .../pump/eopatch/keys/EopatchBooleanKey.kt | 6 +- .../aaps/pump/eopatch/keys/EopatchIntKey.kt | 6 +- .../equil/keys/EquilBooleanPreferenceKey.kt | 6 +- .../pump/equil/keys/EquilIntPreferenceKey.kt | 6 +- .../pump/insight/keys/InsightBooleanKey.kt | 9 +- .../aaps/pump/insight/keys/InsightIntKey.kt | 6 +- .../keys/MedtronicBooleanPreferenceKey.kt | 11 +- .../keys/MedtronicIntPreferenceKey.kt | 11 +- .../keys/MedtronicStringPreferenceKey.kt | 11 +- .../service/RileyLinkMedtronicService.kt | 4 +- .../service/RileyLinkMedtronicServiceUTest.kt | 14 +- .../pump/medtrum/keys/MedtrumBooleanKey.kt | 9 +- .../aaps/pump/medtrum/keys/MedtrumIntKey.kt | 9 +- .../pump/medtrum/keys/MedtrumStringKey.kt | 9 +- .../common/keys/DashBooleanPreferenceKey.kt | 6 +- .../keys/OmnipodBooleanPreferenceKey.kt | 7 +- .../common/keys/OmnipodIntPreferenceKey.kt | 7 +- .../eros/keys/ErosBooleanPreferenceKey.kt | 8 +- .../service/RileyLinkOmnipodService.java | 5 +- .../compose/RileyLinkPairWizardViewModel.kt | 3 +- .../hw/rileylink/keys/RileyLinkStringKey.kt | 3 +- .../keys/RileyLinkStringPreferenceKey.kt | 15 +- .../keys/RileylinkBooleanPreferenceKey.kt | 11 +- .../service/RileyLinkBroadcastReceiver.kt | 4 +- .../app/aaps/ui/search/SearchIndexBuilder.kt | 20 +- 81 files changed, 1023 insertions(+), 483 deletions(-) create mode 100644 core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt create mode 100644 core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 0c756ecfa7ed..44abf7f5d9a4 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -24,18 +24,18 @@ Multiplatform for the UI**, done step by step, starting with a small working sli ## 2. What is already fine -| Area | State | Why it matters | -|--------------------------------|------------------------------------------------------------------------------------|------------------------------------| +| Area | State | Why it matters | +|--------------------------------|--------------------------------------------------------------------------------------------------|----------------------------------------| | `:core:data` | 58 files, plain `java-library`, **0** Android imports. Two `expect`/`actual` away, see section 8 | The first module to make multiplatform | -| `:core:keys` | 46 files, **0** Android and **0** `java.*` imports since Wave 3 | Only the 381 `R.string` ids block it | -| Room | 46 DAOs, **0** RxJava return types, 22 `suspend`, already on `BundledSQLiteDriver` | This is exactly the Room KMP setup | -| Compose | **0** XML layouts in `:core:ui`, 2 left in `:ui` | Compose Multiplatform can use this | -| `LocalContext.current` | 7 in `:core:ui`, 8 in `:ui` | Very small coupling to Android | -| Network code | 5 files touch Retrofit / OkHttp, 1 touches socket.io | REST part is small enough for Ktor | -| kotlinx.serialization | 64 files (Gson: 34) | Already the main choice | -| kotlinx-datetime | Declared in `libs.versions.toml` | Ready to use | -| `androidx.lifecycle` ViewModel | Multiplatform since 2.8 | Most of the 129 uses are fine | -| Vico charts | Ships a `multiplatform` artifact | Only the artifact name changes | +| `:core:keys` | 46 files, **0** Android and **0** `java.*` imports since Wave 3 | Only the 381 `R.string` ids block it | +| Room | 46 DAOs, **0** RxJava return types, 22 `suspend`, already on `BundledSQLiteDriver` | This is exactly the Room KMP setup | +| Compose | **0** XML layouts in `:core:ui`, 2 left in `:ui` | Compose Multiplatform can use this | +| `LocalContext.current` | 7 in `:core:ui`, 8 in `:ui` | Very small coupling to Android | +| Network code | 5 files touch Retrofit / OkHttp, 1 touches socket.io | REST part is small enough for Ktor | +| kotlinx.serialization | 64 files (Gson: 34) | Already the main choice | +| kotlinx-datetime | Declared in `libs.versions.toml` | Ready to use | +| `androidx.lifecycle` ViewModel | Multiplatform since 2.8 | Most of the 129 uses are fine | +| Vico charts | Ships a `multiplatform` artifact | Only the artifact name changes | The biggest surprise was `:core:ui`. Of its 432 files: @@ -53,30 +53,30 @@ So most of the Compose work of the last year can be reused. ### Libraries with no Kotlin/Native version -| Library | Files | Replacement | -|--------------------------------------------------------|--------------------------------------------------------|----------------------------------------| -| Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | -| RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | +| Library | Files | Replacement | +|--------------------------------------------------------|--------------------------------------------------------|-----------------------------------------| +| Dagger / Hilt | ~300 | kotlin-inject, Metro, or Koin | +| RxJava 3 | RxBus in 35 `:ui`, 19 config, 18 sync, 13 impl, 12 aps | SharedFlow | | Retrofit + OkHttp | 9 / 11, **0 in `:core:nssdk`** | Ktor client - done there, see section 8 | -| socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | -| Gson | 46, **0 in `:core:nssdk`** | kotlinx.serialization - see section 8 | -| `org.json` (`JSONObject`) | 227, **0 in `:core:nssdk`** | kotlinx `JsonObject` - see section 8 | -| WorkManager | 44 | See warning below | -| joda-time | 5 | kotlinx-datetime | -| `java.text.DecimalFormat` | 74 | Done, see section 8 | -| `java.text.SimpleDateFormat` | 7 | kotlinx-datetime formatting | -| `java.util.concurrent.TimeUnit` | 108 files, but only 93 sites convertible | `kotlin.time.Duration` | -| `Executors`, `ConcurrentHashMap` | 14 | Coroutine dispatchers, map plus mutex | -| `java.security`, `javax.crypto` | ~20 | cryptography-kotlin, or expect/actual | -| `java.io.File` | 12 | okio | -| spongycastle, tink-android | 2 | as above | -| commons-lang3, Guava | 4 | Inline the few helpers | -| slf4j, logback-android | 4 | Kermit or Napier | -| kotlin-reflect | 9 | No reflection on Native, must go | -| Firebase | 4 | GitLive Firebase KMP, or expect/actual | -| Play Services | 2 | Android only by nature | -| androidx.glance (widgets) | 7 | WidgetKit is Swift only, no reuse | -| Garmin, osmdroid, androidsvg, appauth, java-otp, zxing | 1-3 each | Mostly not client features | +| socket.io-client | 1 (`NSClientV3Service`) | **Do not replace** - see section 3a | +| Gson | 46, **0 in `:core:nssdk`** | kotlinx.serialization - see section 8 | +| `org.json` (`JSONObject`) | 227, **0 in `:core:nssdk`** | kotlinx `JsonObject` - see section 8 | +| WorkManager | 44 | See warning below | +| joda-time | 5 | kotlinx-datetime | +| `java.text.DecimalFormat` | 74 | Done, see section 8 | +| `java.text.SimpleDateFormat` | 7 | kotlinx-datetime formatting | +| `java.util.concurrent.TimeUnit` | 108 files, but only 93 sites convertible | `kotlin.time.Duration` | +| `Executors`, `ConcurrentHashMap` | 14 | Coroutine dispatchers, map plus mutex | +| `java.security`, `javax.crypto` | ~20 | cryptography-kotlin, or expect/actual | +| `java.io.File` | 12 | okio | +| spongycastle, tink-android | 2 | as above | +| commons-lang3, Guava | 4 | Inline the few helpers | +| slf4j, logback-android | 4 | Kermit or Napier | +| kotlin-reflect | 9 | No reflection on Native, must go | +| Firebase | 4 | GitLive Firebase KMP, or expect/actual | +| Play Services | 2 | Android only by nature | +| androidx.glance (widgets) | 7 | WidgetKit is Swift only, no reuse | +| Garmin, osmdroid, androidsvg, appauth, java-otp, zxing | 1-3 each | Mostly not client features | ### Android framework @@ -120,11 +120,11 @@ means giving up push updates, which is the whole point of NSClientV3. So this dependency is **abstracted, not removed**: -| Where | What | -| --- | --- | -| Android | keep `io.socket:socket.io-client:2.1.2` **unchanged** | -| iOS | `socket.io-client-swift` | -| shared | a small `expect interface` over the calls actually used | +| Where | What | +|---------|---------------------------------------------------------| +| Android | keep `io.socket:socket.io-client:2.1.2` **unchanged** | +| iOS | `socket.io-client-swift` | +| shared | a small `expect interface` over the calls actually used | Both clients are written by the Socket.IO project itself, so protocol compatibility follows the server rather than a third party's reimplementation. That matters more here than saving a @@ -152,12 +152,12 @@ reimplementation, which is the wrong direction of risk for a medical app. Since the plan is to write our own small wrapper rather than depend on one, the useful reading is the existing wrappers' source, because they already are the `expect` / `actual` we would write: -| Link | Why | -| --- | --- | -| [moko-socket-io] | Cleanest reference. Small, `expect class Socket` over the two native clients. | -| [KotSock] | Same approach, a second opinion on the API shape. | -| [moko-socket-io-sample] | A working KMP app using it, by the moko maintainer. Shows the CocoaPods wiring for `socket.io-client-swift`. | -| [KMP Socket.IO deep dive] | Walks through building exactly this kind of wrapper. Closest thing to the POC we would be reproducing. | +| Link | Why | +|---------------------------|--------------------------------------------------------------------------------------------------------------| +| [moko-socket-io] | Cleanest reference. Small, `expect class Socket` over the two native clients. | +| [KotSock] | Same approach, a second opinion on the API shape. | +| [moko-socket-io-sample] | A working KMP app using it, by the moko maintainer. Shows the CocoaPods wiring for `socket.io-client-swift`. | +| [KMP Socket.IO deep dive] | Walks through building exactly this kind of wrapper. Closest thing to the POC we would be reproducing. | **Check the dates before trusting any of them as a dependency.** moko-socket-io states Gradle 6.8+, Android API 16+, iOS 11.0+ - those baselines are from around 2021, while this project is on Gradle @@ -165,10 +165,15 @@ Android API 16+, iOS 11.0+ - those baselines are from around 2021, while this pr problem at all for reading it as a pattern. [moko-socket-io]: https://github.com/icerockdev/moko-socket-io + [moko-socket-io-sample]: https://github.com/Alex009/moko-socket-io-sample + [KotSock]: https://github.com/whiterabb17/KotSock + [dyte-io/socketio-kotlin]: https://github.com/dyte-io/socketio-kotlin + [kmp-socketio]: https://klibs.io/project/HackWebRTC/kmp-socketio + [KMP Socket.IO deep dive]: https://rahuljindaltech.medium.com/building-cross-platform-libraries-with-kotlin-multiplatform-kmp-kmm-a-deep-dive-into-socket-io-89e58c3b221c ### Protocol versions - checked @@ -176,12 +181,12 @@ problem at all for reading it as a pattern. Socket.IO major versions are **not** wire compatible, so the server and both clients have to agree. Checked on 2026-08-05: -| Part | Version | Protocol | -| --- | --- | --- | -| Nightscout `cgm-remote-monitor` 15.0.7 | `socket.io ~4.5.4` | **v4** | -| AAPS today, Android | `io.socket:socket.io-client:2.1.2` | v3 / v4 - correct | -| iOS should use | `Socket.IO-Client-Swift` **16.x** | v3 / v4 | -| moko-socket-io 0.6.0, iOS half | `Socket.IO-Client-Swift ~> 15.2.0` | **v2 - will not talk to Nightscout** | +| Part | Version | Protocol | +|----------------------------------------|------------------------------------|--------------------------------------| +| Nightscout `cgm-remote-monitor` 15.0.7 | `socket.io ~4.5.4` | **v4** | +| AAPS today, Android | `io.socket:socket.io-client:2.1.2` | v3 / v4 - correct | +| iOS should use | `Socket.IO-Client-Swift` **16.x** | v3 / v4 | +| moko-socket-io 0.6.0, iOS half | `Socket.IO-Client-Swift ~> 15.2.0` | **v2 - will not talk to Nightscout** | `socket.io-client-java` 2.x speaks Socket.IO 3 / 4. On the Swift side that generation only arrives in **16.0.0** (February 2024, "now supports Socket.IO 3 servers"); 15.x is Socket.IO 2 only. @@ -344,7 +349,16 @@ existing translations are untouched. 2. **iOS needs `CFBundleLocalizations` in `Info.plist`.** Without it iOS reports the wrong preferred language and resource lookup quietly falls back to English. 3. **`Res` is generated per module**, so `:core:ui` and `:ui` each have their own. Same situation as - today with `app.aaps.core.ui.R` and `app.aaps.ui.R`. + today with `app.aaps.core.ui.R` and `app.aaps.ui.R`. It also needs `publicResClass = true`, or + the + generated `Res` is internal and another module cannot see it. +4. **No public locale override.** `ResourceEnvironment` has an `internal` constructor and + `getSystemResourceEnvironment()` takes no parameters, so there is no supported way to ask for " + the + English text" while the UI is in another language. `SearchIndexBuilder` does exactly that, + through + `rh.gsNotLocalised(id)`, so for that one caller compose-resources is a feature loss rather than a + port. This is the problem that decided wave 10. ### What this means for `:core:keys` @@ -352,6 +366,15 @@ The 368 strings themselves move without trouble. The real work is that the key c `@StringRes Int`, and a Compose Multiplatform resource is a `StringResource`, not an `Int`. That type change is the job, not the translations. +**Done, in wave 10 - but not with `StringResource`.** The key classes now carry `TextRef`, a two +case +sealed interface owned by `:core:keys` with no Android and no compose-resources types in it. Putting +`StringResource` directly on the keys was tried and rejected: problem 1 above is fatal for it, and +there is a fourth problem the list above misses - compose-resources has **no public locale override +**, +so the always-English search index could not be built at all. `TextRef` moves the type change to the +call sites once, and leaves the choice of resource system to each module, later. + --- ## 7. Suggested order of work @@ -376,17 +399,24 @@ and the next screen, not because it is on a list. **Where this stands.** Steps 1 and 5 are **done**, and step 0 is half done - out of order, because `:core:nssdk` turned out to be sliceable after all. Two modules now build for Kotlin/Native: -| Module | State | -| --- | --- | -| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | +| Module | State | +|---------------|-----------------------------------------------------| +| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | | `:core:nssdk` | multiplatform, 72 files in `commonMain` (waves 6-9) | -Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Gson, joda, Retrofit, +Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Gson, joda, +Retrofit, OkHttp, `android.*` and `java.io` are all gone from it. -That leaves steps 2, 3, 4 and 6, and the half of step 0 that **needs a Mac**: a real device, and an -honest look at how Compose Multiplatform feels on iOS. No amount of further blocker removal answers -that question, which is the argument for doing it soon rather than continuing down the list. +Step 2 is now half done as well: `:core:keys` no longer hands out bare resource ids, it hands out +`TextRef` (wave 10). The ids are still AAPT ids inside it, so this is the seam rather than the +move - +but it is the half that had to come first, because it is the half that touches every call site. + +That leaves the rest of steps 2, 3, 4 and 6, and the half of step 0 that **needs a Mac**: a real +device, and an honest look at how Compose Multiplatform feels on iOS. No amount of further blocker +removal answers that question, which is the argument for doing it soon rather than continuing down +the list. --- @@ -394,27 +424,27 @@ that question, which is the argument for doing it soon rather than continuing do Committed on `dev`: -| Commit | What | -| --- | --- | -| `e5f4e27626` | Migrate DecimalFormat | -| `a42d823c93` | Eliminate TimeUnit | -| `e1068e77db` | `:core:keys` remove JVM dependency | -| `35b5399798` | Extract dependencies | +| Commit | What | +|--------------|-------------------------------------------------------| +| `e5f4e27626` | Migrate DecimalFormat | +| `a42d823c93` | Eliminate TimeUnit | +| `e1068e77db` | `:core:keys` remove JVM dependency | +| `35b5399798` | Extract dependencies | | `1aed547f7a` | cleanup (`TB.isInProgress` moved out of `:core:data`) | Committed on `kmp/core-data-experiment`, a throwaway branch kept because the work turned out to be worth keeping (see waves 5 and 6): -| Commit | What | -| --- | --- | -| `67ecb1e696` | Going KMP - `:core:data` is a real multiplatform module | -| `e24476237b` | Prepare `:core:nssdk` - dead code out, defaults in, characterization tests | -| `f6a4e85e8a` | `:core:nssdk` `org.json` -> kotlinx | -| `350f486be4` | `:core:nssdk` Gson -> kotlinx.serialization | -| `cb7b8fa924` | `:core:nssdk` date parsing, eliminate joda | +| Commit | What | +|--------------|------------------------------------------------------------------------------------| +| `67ecb1e696` | Going KMP - `:core:data` is a real multiplatform module | +| `e24476237b` | Prepare `:core:nssdk` - dead code out, defaults in, characterization tests | +| `f6a4e85e8a` | `:core:nssdk` `org.json` -> kotlinx | +| `350f486be4` | `:core:nssdk` Gson -> kotlinx.serialization | +| `cb7b8fa924` | `:core:nssdk` date parsing, eliminate joda | | `e92ec082d3` | `:core:nssdk` tests - the contract suite, written against Retrofit before the port | -| `ed9c87599f` | `:core:nssdk` Ktor migration | -| `37e146861f` | version `4.0.0-dev-b-kmp` | +| `ed9c87599f` | `:core:nssdk` Ktor migration | +| `37e146861f` | version `4.0.0-dev-b-kmp` | ### Wave 1 - `DecimalFormat` removed @@ -603,14 +633,16 @@ is the wrong shape here: on `:core:data`, so the reverse edge is a cycle. And a data class default parameter has nowhere to receive an injected dependency anyway. - The ripple is large. The compiler first reported 2 errors; the real count was 38 and still growing - when the attempt was stopped, with test sources not yet compiled. Gradle stops at the first failing + when the attempt was stopped, with test sources not yet compiled. Gradle stops at the first + failing module and Kotlin caps reported errors per file - in `PersistenceLayerImpl` it named 10 of 31. - **`utcOffset` is not a throwaway field.** It is stored in every table, it is part of `contentEqualsTo` (so a changed value makes a record compare unequal and re-sync), it is uploaded to Nightscout - where the server validates it and `NSAndroidClientImpl` has to catch `"Bad or missing utcOffset field"` 400s and retry with 0 - it goes to Open Humans, and it is shown in the user entry history. Recomputing it by hand at 50+ sites risks silently changing stored - values; `expect` / `actual` keeps the computation identical at every site and changes no call site. + values; `expect` / `actual` keeps the computation identical at every site and changes no call + site. So when `:core:data` becomes multiplatform: @@ -627,13 +659,13 @@ creation is the weaker case, and the 154 call sites that care already pass `utcO **State of `:core:data` now:** -| Was | Now | -| --- | --- | -| `System.currentTimeMillis()` | gone (`T.now()` deleted, `isInProgress` moved out) | -| `java.util.Locale` | gone (`DoseStepSize`) | -| `java.util.concurrent.TimeUnit` | gone (Wave 2) | -| `java.util.TimeZone`, 17 files | `expect` / `actual`, no call site changes | -| `NumberFormatPlatform` | `expect` / `actual`, by design | +| Was | Now | +|---------------------------------|----------------------------------------------------| +| `System.currentTimeMillis()` | gone (`T.now()` deleted, `isInProgress` moved out) | +| `java.util.Locale` | gone (`DoseStepSize`) | +| `java.util.concurrent.TimeUnit` | gone (Wave 2) | +| `java.util.TimeZone`, 17 files | `expect` / `actual`, no call site changes | +| `NumberFormatPlatform` | `expect` / `actual`, by design | Two `expect` / `actual` declarations away from compiling as `commonMain`. @@ -649,9 +681,9 @@ That was the single most valuable thing to learn, and the reason it was worth do Two `expect` / `actual` seams, both deliberate: -| Seam | Why | -| --- | --- | -| `NumberFormatPlatform` | number formatting is genuinely platform work | +| Seam | Why | +|--------------------------------|--------------------------------------------------------------------------| +| `NumberFormatPlatform` | number formatting is genuinely platform work | | `systemUtcOffsetAt(timestamp)` | replaces `TimeZone.getDefault()` in 17 model files, no call site changed | The `mingwX64` target needed one opt-in, `kotlin.experimental.ExperimentalNativeApi`, because @@ -670,7 +702,8 @@ untouched. The lesson generalises: "these things are coupled" is a claim about o worth checking whether some other axis cuts cleanly before accepting a big-bang migration. `org.json` is a JVM and Android API. It was in the **public API** of `NSAndroidClient` (10 methods) -and `RunningConfiguration` (2), carrying profiles and settings - the two document kinds AAPS does not +and `RunningConfiguration` (2), carrying profiles and settings - the two document kinds AAPS does +not model, because it does not own their shape. It is now gone from the module. Why it could be done alone: both directions already round-tripped through text, so `org.json` was @@ -682,18 +715,19 @@ api.createSetting(JsonParser.parseString(doc.toString())) // write - org.jso ``` Swapping the carrier for kotlinx `JsonObject` left the write line **character for character -identical** and changed one parse call on the read side. Gson, Retrofit and the 210 `@SerializedName` +identical** and changed one parse call on the read side. Gson, Retrofit and the 210 +`@SerializedName` annotations were not touched. **The trap, and why tests came first.** The two libraries disagree about a missing key, and nothing about the difference shows up at compile time: -| accessor | `org.json`, missing key | `org.json`, explicit `null` | kotlinx, missing key | -| --- | --- | --- | --- | -| `optString` | `""` | `"null"` - the four letter text | `null` | -| `optJSONObject` | `null` | `null` | `null` | -| `optLong(k, 0)` | `0` | `0` | `null` | -| `optBoolean` | `false` | `false` | `null` | +| accessor | `org.json`, missing key | `org.json`, explicit `null` | kotlinx, missing key | +|-----------------|-------------------------|---------------------------------|----------------------| +| `optString` | `""` | `"null"` - the four letter text | `null` | +| `optJSONObject` | `null` | `null` | `null` | +| `optLong(k, 0)` | `0` | `0` | `null` | +| `optBoolean` | `false` | `false` | `null` | A straight translation would silently flip every downstream `isEmpty()` and `?:`. So the swap was done through `OrgJsonCompat`, kotlinx accessors that reproduce `org.json` exactly, golden-mastered @@ -701,14 +735,16 @@ against the real thing over 21 inputs x 5 accessors. The type change is then equ construction rather than by inspection. The one genuine divergence: reading an **object** through `optString` differs in key order, because -`org.json` iterates a hash map and its order is unspecified. No call site does it - all six keys read +`org.json` iterates a hash map and its order is unspecified. No call site does it - all six keys +read through `optString` hold strings - so it is documented and skipped rather than pinned. **What stays on `org.json`, on purpose.** `JsonBridge` marks the two boundaries: - **socket.io** hands every payload over as `org.json.JSONObject`. That is the library's API, so the conversion happens as the event arrives and everything downstream is kotlinx. -- **The profile subsystem** - `ProfileStore`, `PureProfile`, `DataSyncSelector.PairProfileStore` - is +- **The profile subsystem** - `ProfileStore`, `PureProfile`, `DataSyncSelector.PairProfileStore` - + is `org.json` throughout `:core:interfaces`. Converting it is its own project. Profiles are converted where they cross into the client instead. @@ -728,28 +764,32 @@ so the `org.json` quirks stay out of the shared modules. **Immutability was the only real code change.** kotlinx `JsonObject` cannot be mutated, so six in-place `put` calls became rebuilds. Five were mechanical; the sixth was not - -`RunningConfigurationPublisher` mutated a *nested* object to mask `NsClientAllowClientControl` inside +`RunningConfigurationPublisher` mutated a *nested* object to mask `NsClientAllowClientControl` +inside `syncedPrefs`, and that is now an explicit rebuild preserving key order and the masking semantics. -The wire format's mixed typing was preserved deliberately: `isFakingTempsByExtendedBoluses` is a real +The wire format's mixed typing was preserved deliberately: `isFakingTempsByExtendedBoluses` is a +real JSON boolean while every `syncedPrefs` value is a string, exactly as `org.json` wrote them. -**Left unfixed, on purpose.** `optStringCompat` faithfully reproduces the `"null"` quirk, so a server +**Left unfixed, on purpose.** `optStringCompat` faithfully reproduces the `"null"` quirk, so a +server sending `{"message":null}` still writes the word "null" into the user visible NSClient log. Changing behaviour during a type migration is how regressions get smuggled in; that is a separate change. **What is left in `:core:nssdk`:** -| Dependency | Files | Slice | -| --- | --- | --- | -| Gson | 17 | the converter switch | -| joda-time | 1 | with the converter - `RemoteTreatment` lenient dates | -| Retrofit | 4 | Ktor | -| OkHttp | 3 | Ktor | -| `java.io.IOException` | 1 | with Ktor - the exception hierarchy | -| `android.*` | 2 | with Ktor - mainly `Context` for the OkHttp disk cache | - -Verified on WSA with a full sync. That specifically exercises the riskiest part: settings and profile +| Dependency | Files | Slice | +|-----------------------|-------|--------------------------------------------------------| +| Gson | 17 | the converter switch | +| joda-time | 1 | with the converter - `RemoteTreatment` lenient dates | +| Retrofit | 4 | Ktor | +| OkHttp | 3 | Ktor | +| `java.io.IOException` | 1 | with Ktor - the exception hierarchy | +| `android.*` | 2 | with Ktor - mainly `Context` for the OkHttp disk cache | + +Verified on WSA with a full sync. That specifically exercises the riskiest part: settings and +profile reads both go through the changed Gson type adapter, and kotlinx `JsonObject` also implements `Map`, so Gson picking its built-in map adapter instead of the registered one was a real possibility that would have failed quietly rather than thrown. @@ -775,13 +815,13 @@ Two things got smaller rather than bigger: `NsSdkJson.kt` holds one `Json` instance shared by Retrofit and the string mappers. Every flag is there to match Gson, not because it is a good default in the abstract: -| Flag | Why | -| --- | --- | -| `ignoreUnknownKeys` | documents carry fields this version has never seen | -| `explicitNulls = false` | Gson omits nulls; NS rejects some explicit nulls | -| `isLenient` | one measured case: a bare **number** arriving in a `String` field (`created_at`) | -| `encodeDefaults` | **backward compatibility** - see below | -| `coerceInputValues` | **forward compatibility** - see below | +| Flag | Why | +|-------------------------|----------------------------------------------------------------------------------| +| `ignoreUnknownKeys` | documents carry fields this version has never seen | +| `explicitNulls = false` | Gson omits nulls; NS rejects some explicit nulls | +| `isLenient` | one measured case: a bare **number** arriving in a `String` field (`created_at`) | +| `encodeDefaults` | **backward compatibility** - see below | +| `coerceInputValues` | **forward compatibility** - see below | Two of those five were added only because a test failed, and both are the difference between working and quietly breaking somebody else's device: @@ -791,7 +831,8 @@ and quietly breaking somebody else's device: `duration = 0`, `utcOffset = 0` and every `LastModified.Collections` counter simply stop being written. Absent is not the same as false to a reader that has not been updated. - **`coerceInputValues`** - an **unknown enum value** throws in kotlinx but was mapped to `null` by - Gson. `eventType` is an enum. A *newer* AAPS adding one event type would otherwise make every older + Gson. `eventType` is an enum. A *newer* AAPS adding one event type would otherwise make every + older client throw on that record, inside a socket.io listener with no try/catch. Only one case needed `isLenient`. Strict kotlinx already accepts a quoted number in a `Long` / @@ -832,27 +873,36 @@ decode test at all** - `FoodExtensionKtTest` is object-to-object and `LoadFoodsW A Pixel emulator ran a **new master against a pre-KMP client** on a live Nightscout instance. Every record type that can be created by hand was created and followed end to end: -| Record | Old client result | -| --- | --- | -| Carbs | `◄ INSERT`, visible in Treatments history | -| Bolus (with nested `icfg`) | `◄ INSERT Bolus`, visible in Treatments history | -| Profile switch | `◄ INSERT ProfileSwitch` + `EffectiveProfileSwitch` | -| Temporary target (new **and** PATCH update) | `◄ INSERT TemporaryTarget` | -| Therapy event | `◄ INSERT TherapyEvent` | -| Glucose, devicestatus, settings | received and applied | +| Record | Old client result | +|---------------------------------------------|-----------------------------------------------------| +| Carbs | `◄ INSERT`, visible in Treatments history | +| Bolus (with nested `icfg`) | `◄ INSERT Bolus`, visible in Treatments history | +| Profile switch | `◄ INSERT ProfileSwitch` + `EffectiveProfileSwitch` | +| Temporary target (new **and** PATCH update) | `◄ INSERT TemporaryTarget` | +| Therapy event | `◄ INSERT TherapyEvent` | +| Glucose, devicestatus, settings | received and applied | The actual bytes are the best evidence. This is what the new master uploaded for a 20 g carb entry: ```json -{"date":1786012366166,"utcOffset":0,"app":"AAPS","isValid":true, - "isReadOnly":false,"eventType":"Meal Bolus","carbs":20.0,"notes":""} +{ + "date": 1786012366166, + "utcOffset": 0, + "app": "AAPS", + "isValid": true, + "isReadOnly": false, + "eventType": "Meal Bolus", + "carbs": 20.0, + "notes": "" +} ``` `utcOffset:0`, `isValid:true` and `isReadOnly:false` are all present - that is `encodeDefaults` working. Without it those three vanish. Nulls are omitted, as Gson did. The strongest single result is the **signed client-control round trip**: the old client sent an -HMAC-signed envelope, the new master verified it and acked, and the old client verified the ack back. +HMAC-signed envelope, the new master verified it and acked, and the old client verified the ack +back. A signature only verifies if the serialized bytes match, so that is close to proof rather than inference. @@ -862,7 +912,8 @@ download-only. The food fix rests on unit tests. ### Wave 8 - `:core:nssdk` off Retrofit and OkHttp (done) -The HTTP client moved to **Ktor on the OkHttp engine**, so the app reuses the OkHttp it already ships +The HTTP client moved to **Ktor on the OkHttp engine**, so the app reuses the OkHttp it already +ships rather than carrying two HTTP stacks. On iOS the engine becomes Darwin and nothing else changes, which is the whole reason for the move. @@ -873,7 +924,8 @@ and each finding was then attacked by an agent trying to refute it. That produce and **30 silent-failure risks**. Three were serious enough to have shipped: - **The 304 signal only existed because of the OkHttp disk cache.** `response.raw().networkResponse - ?.code` can only be 304 when the cache revalidates a GET and merges the result into a 200. Ktor has + ?.code` can only be 304 when the cache revalidates a GET and merges the result into a 200. Ktor + has no equivalent, so `code` would have become a constant 200 - and `LoadBgWorker`'s `response.code != 304 && processSgvs(...)` would have become an unconditional process. The paging loop never exits, `storeGlucoseValuesToDb()` is never reached, and **BG never lands in the @@ -882,16 +934,19 @@ and **30 silent-failure risks**. Three were serious enough to have shipped: `v3/profile?sort$desc=date&limit=1`. Rebuild the URL from the method parameters and they vanish. Losing `limit=1` there makes `LoadProfileStoreWorker` take `profiles[profiles.size - 1]` from an unsorted list, silently applying the **wrong profile**: different basal, ISF and IC. -- **The `utcOffset` auto-retry reads the raw 400 body text.** Ktor has no separate `errorBody()`, and +- **The `utcOffset` auto-retry reads the raw 400 body text.** Ktor has no separate `errorBody()`, + and a body read lazily or only on success loses that string. The record is then dropped permanently while the sync cursor advances. #### Tests before code, against the old stack -49 characterization tests were written **while Retrofit was still in place**, using **MockWebServer** +49 characterization tests were written **while Retrofit was still in place**, using **MockWebServer +** + - a real HTTP server on localhost - rather than Ktor's `MockEngine`. That choice is the point: -`MockEngine` only exists after the swap, so tests written with it could never have proved anything -about the behaviour before it. The same files ran unchanged against both stacks. + `MockEngine` only exists after the swap, so tests written with it could never have proved anything + about the behaviour before it. The same files ran unchanged against both stacks. They pin: every endpoint URL as a literal string; the read/write status asymmetry and its **request counts**; the `utcOffset` fallback verified by inspecting both request bodies; the auth headers, @@ -900,9 +955,9 @@ inconsistently per endpoint. Two divergences were caught this way rather than in the field: -| | | -| --- | --- | -| `$` in query keys | survived on the first try - Ktor's `encodedParameters` leaves it literal | +| | | +|---------------------------|---------------------------------------------------------------------------------------| +| `$` in query keys | survived on the first try - Ktor's `encodedParameters` leaves it literal | | `/` inside a path segment | Ktor does **not** encode it, Retrofit does (`a%2Fb`). Fixed with `encodeSlash = true` | #### Decisions worth recording @@ -917,18 +972,22 @@ Two divergences were caught this way rather than in the field: with the anonymous role**, and uploads stop with nothing logged), and it refreshes on 401 only, while Nightscout also answers 403. It also never sees the response body, so the clock-skew error could not exist. -- **The 304 brake was replaced, not reproduced.** The workers now stop when the cursor cannot advance +- **The 304 brake was replaced, not reproduced.** The workers now stop when the cursor cannot + advance (`lastServerModified <= lastLoaded`), which is what the 304 always meant. Only the modified-since path can stall that way; the first load pages by date and carries no server timestamp. - **`close()` was added** to `NSAndroidClient` and is called from `restartOnChange`. The Retrofit client leaked quietly on every URL or token change; a Ktor engine holds real connections. Also fixed on the way, deliberately and separately: `getVersion` / `getStatus` threw -`retrofit2.HttpException` and `NullPointerException`, neither a `NightscoutException`; they now throw -`UnsuccessfulNightscoutException`. And a 10 s connect timeout is now explicit - OkHttp applied one by +`retrofit2.HttpException` and `NullPointerException`, neither a `NightscoutException`; they now +throw +`UnsuccessfulNightscoutException`. And a 10 s connect timeout is now explicit - OkHttp applied one +by default and Ktor does not, so a dead host would otherwise hang for the full 60 s socket timeout. -**Left broken on purpose:** `updateFood` and `deleteFood` declare an `{identifier}` the path does not +**Left broken on purpose:** `updateFood` and `deleteFood` declare an `{identifier}` the path does +not contain, so Retrofit refused to build them and **no request was ever sent** - food edits have never synced, because the endpoint is broken on the Nightscout side. A hand-written Ktor URL would have turned "never sends" into `PATCH /api/v3/food` and `DELETE /api/v3/food` **with no identifier**, a @@ -938,7 +997,8 @@ verified by a test asserting zero requests. #### Verified on a device A new Ktor master against a **pre-KMP client** on a live Nightscout: writes accepted (201), reads -across status / lastModified / settings / devicestatus / treatments, socket.io push received, and the +across status / lastModified / settings / devicestatus / treatments, socket.io push received, and +the old Gson/Retrofit client stored what Ktor wrote. The auth flow ran for real - a 401 triggered `GET /api/v2/authorization/request/` and a 200 - which exercised the hand-written interceptor and the leading-slash refresh URL together. @@ -959,12 +1019,12 @@ in `commonMain`**, and all four consumer modules build unchanged. Only four things blocked `commonMain`, and none needed design work: -| Blocker | Fix | -| --- | --- | -| `@JvmSynthetic` on an `internal` helper | dropped - it was hiding an already-internal function from Java | +| Blocker | Fix | +|----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@JvmSynthetic` on an `internal` helper | dropped - it was hiding an already-internal function from Java | | `KClass` in `retry` | `KClass`. Exact-class matching and the `catch (Exception)` are unchanged, so which exceptions are excluded and how many attempts happen stay identical | -| `Dispatchers.IO`, 2 sites | `expect val nsIoDispatcher`; the JVM actual is the same `Dispatchers.IO` as before | -| the Ktor engine and its logging interceptor | `expect fun nsHttpClient(...)` - OkHttp on JVM, CIO on Native | +| `Dispatchers.IO`, 2 sites | `expect val nsIoDispatcher`; the JVM actual is the same `Dispatchers.IO` as before | +| the Ktor engine and its logging interceptor | `expect fun nsHttpClient(...)` - OkHttp on JVM, CIO on Native | #### The crypto did not need solving @@ -1003,15 +1063,142 @@ through every caller. #### Honest limits of the proof `mingwX64` is a **compile proof, not a shipping target**: request logging is not wired up there, and -CIO stands in for what an iOS build would use (Darwin). What it does prove is real - 72 files of wire +CIO stands in for what an iOS build would use (Darwin). What it does prove is real - 72 files of +wire layer, models, mappers and HTTP client with no JVM API anywhere in them. +### Wave 10 - `TextRef`, a seam for strings (phase 1 done) + +Every user facing string in AAPS is an Android `R.string` id: a plain `Int` handed out by AAPT at +build time. The preference keys alone carry 394 of them - a title and often a summary on each of +about 300 constants, spread over 40 enums in `:core:keys` and the plugins. An iOS or desktop client +has no AAPT and therefore no such ids, so this is the next real blocker after the network layer. + +#### The obvious answer does not fit + +The KMP answer is Compose Multiplatform Resources: keep the same `strings.xml` and the same +`values-XX` folders, let the Gradle plugin generate a `Res` object per module, and read strings with +`stringResource(Res.string.x)` in Compose or `getString(Res.string.x)` outside it. Translation stays +in Crowdin exactly as it is now, which was the main worry and turned out to be a non-issue. + +Two things stop it from going on the key classes themselves: + +- **`getString()` is `suspend`.** In Compose that is fine. Outside Compose it is not: a preference + key's title is read from view models, notification builders and the search index, and none of them + are suspend today. +- **There is no public way to ask for a specific locale.** `SearchIndexBuilder` builds a second, + always-English index so a user with a translated UI can still search using the English term they + read in the docs. Today that is `rh.gsNotLocalised(id)`. compose-resources has a + `ResourceEnvironment`, but its constructor is `internal` and `getSystemResourceEnvironment()` + takes + no parameters, so the English index cannot be built at all. That is a feature loss, not a port. + +moko-resources was evaluated as the alternative and rejected: it solves the suspend problem and has +a +real locale override, but it is a third party plugin, it wants its own `MR` object and its own +`StringDesc`, and adopting it means every module on the KMP path takes a dependency on it before any +of them is actually multiplatform. + +#### What was built instead + +`TextRef` is a two case sealed interface in `:core:keys`, with no Android types in it: + +```kotlin +sealed interface TextRef { + data class Res(val id: Int, val args: List = emptyList()) : TextRef + data class Literal(val text: String) : TextRef +} +``` + +It is deliberately **not** a resource system. It is a seam. `PreferenceKey.titleResId: Int` became +`PreferenceKey.title: TextRef`, and `summaryResId: Int?` became `summary: TextRef?`, so nothing +outside the resolvers passes a bare resource id around any more. There are exactly two resolvers: + +- `app.aaps.core.ui.compose.stringResource(ref)` for Compose, +- `ResourceHelper.gs(ref)` / `gsNotLocalised(ref)` for everything else, both default methods on the + interface so no implementation had to change. + +Today `TextRef.Res.id` is always an ordinary AAPT id and both resolvers just pass it through. When a +module later does move its strings to `commonMain/composeResources`, that module's ids can be +encoded as negative tokens and only these two functions learn about it. The call sites do not change +a second time. + +`TextRef.Literal` is the other half, and it is what makes the type worth having rather than a +`typealias`: some titles are not resources at all. It also collapses the "resource id **or** already +resolved string" property pairs that had started to appear. + +#### Phase 1, and what it cost + +The 40 preference key enums were converted by script, and every enum **constant** is byte for byte +unchanged - they still say `titleResId = R.string.x`. Only the constructor parameter turned private +and two property overrides were added after the constants: + +```kotlin +override val title: TextRef = TextRef.Res(titleResId) +override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +``` + +That kept all 272 constants and their 394 resource references untouched. `PreferenceSubScreenDef` +got +the same treatment for the same reason - roughly fifty plugin call sites build it with +`titleResId = R.string.x`. + +`:core:interfaces` had to change `implementation(project(":core:keys"))` to `api`, because `TextRef` +now appears in a `ResourceHelper` signature. That is not a new module edge, only a widened one. + +The `if (titleResId == 0) return` guards in nine composables went with it: a title is now a non-null +`TextRef`, so "no title" is not representable at the call site. + +**One of those guards was not dead, and the first check for that was wrong.** Grepping for +`titleResId = 0` finds nothing, which is what the guards were removed on - but 26 key enums +*defaulted* the parameter to `0`, and one constant took the default: +`RileyLinkStringPreferenceKey.MacAddress`. It is storage, not a preference - the pairing wizard +writes it and it is on no screen - so the composables never saw it, but it is registered, so it did +reach `SearchIndexBuilder` and would have been indexed with an empty title. + +Rather than guard it, the key moved to where it belonged: `RileyLinkStringKey`, the +`StringNonPreferenceKey` enum sitting next to it that already held the RileyLink device `Name`. The +preference key string, the default and `exportable` are unchanged, so a paired RileyLink keeps its +address and settings export is unaffected - `isExportableKey()` walks all registered enums, and both +were already registered together. `getAllPreferenceKeys()` filters to `PreferenceKey`, so the key +simply stops reaching the index. No guard, no magic number. + +With the last user of the default gone, `titleResId: Int = 0` was removed from all 26 enums, so the +compiler now rejects a preference key declared without a title. That is the part worth keeping: the +trap cannot come back. + +#### A sentinel that had been waiting to bite + +`SearchableItem.Wiki` carries a page title that comes from the ReadTheDocs API, so it is plain text, +not a resource. It was stored as `titleResId = 0`, and `SearchIndexBuilder.safeGetString()` maps `0` +to `""` - which would make every wiki hit unfindable by its own title. + +It never fired. Wiki results are a live search, not part of the built index: `WikiSearchRepository` +fills `SearchIndexEntry` itself, with `localizedTitle = title`, and never calls +`createIndexEntry()`. +So the `0` sat there unread, correct only by the accident that nothing looked at it. It is now +`TextRef.Literal(wikiTitle)`, and the snippet is the summary, which is what both fields meant all +along. The point is not a fixed bug - it is that the sentinel could not survive the type change, +whereas an `Int` field will hold a lie indefinitely. + +#### Deliberately left for later + +`IntPreferenceKey.entries: Map` and `resolvedEntries: Map?` are the same +resource-id-or-string pair that `TextRef` exists to collapse, and collapsing them would delete +`IntKeyWithEntries` and `withEntries` entirely. It is not in phase 1 because it touches plugin call +sites, and phase 1 was scoped to `:core:*`. + +`UnitType.valueResId()` / `rangeResId()` stay `Int?`. They are format templates that need arguments +supplied at the call site, they live in `:core:ui`, and they are only ever read from Compose - so +routing them through `TextRef` would add a hop and remove nothing. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old and new differ, with a second pass trying to refute what they found. Results worth keeping: -- **`DecimalFormat` -> `NumberFormat`: identical.** 17 patterns against ~90 values across 57 locales, +- **`DecimalFormat` -> `NumberFormat`: identical.** 17 patterns against ~90 values across 57 + locales, then all 17 against every `Locale.getAvailableLocales()` (~800). Zero differences, including NaN, infinities, -0.0, 1e300 and the half-even boundaries. - **`TimeUnit` -> `kotlin.time`: identical where it can be reached.** Checked by loading the real @@ -1027,14 +1214,15 @@ and new differ, with a second pass trying to refute what they found. Results wor Three real changes, all of them fixes, and one bug that the audit caught and that is now fixed (the formatter cache, above): -| Change | Effect | -| --- | --- | -| `qs()` no longer groups | `1234.5` with 1 digit: en `1,234.5` -> `1234.5`, de `1.234.5` -> `1234.5` | +| Change | Effect | +|-------------------------------------|----------------------------------------------------------------------------| +| `qs()` no longer groups | `1234.5` with 1 digit: en `1,234.5` -> `1234.5`, de `1.234.5` -> `1234.5` | | `formatUS` uses `Locale.US` symbols | correct in Arabic; also swaps U+2212 for `-` on sv, nb, lt, hr, fi, sl, et | -| automation `%d` patterns | the literal-digit bug is gone, but nothing renders it | +| automation `%d` patterns | the literal-digit bug is gone, but nothing renders it | `qs()` also gained a `max(0, digits)` guard: `DecimalFormat` used to clamp a negative -`maximumFractionDigits` to 0, while `NumberFormat` throws. No caller passes a negative, but the guard +`maximumFractionDigits` to 0, while `NumberFormat` throws. No caller passes a negative, but the +guard keeps the old contract on a public interface method. --- @@ -1049,24 +1237,31 @@ keeps the old contract on a public interface method. 3. ~~Wave 2 size?~~ **Decided: convert `TimeUnit` only, leave `T` alone.** `TimeUnit` is a real blocker and had to go. `T` is not, so replacing it would have been style work paid for with about 221 build warnings. The dead `T.now()` was removed instead, which is a real cleanup with no cost. -4. ~~Use `DateUtil` instead of `System.currentTimeMillis()` everywhere?~~ **Decided: only where it is - needed.** The principle is right - `DateUtil` is an interface, so it mocks in tests and can differ +4. ~~Use `DateUtil` instead of `System.currentTimeMillis()` everywhere?~~ **Decided: only where it + is + needed.** The principle is right - `DateUtil` is an interface, so it mocks in tests and can + differ per platform - but the sweep would be **547 sites in 227 files**, plus 62 `TimeZone.getDefault()` in 57 files, and most of it sits in pump drivers that will never be KMP. Do it in the modules on the KMP path, leave `:pump:*` and `:wear` alone, and make it a rule for new code so the number stops growing. -5. ~~Which branch for the first KMP module?~~ **Decided: `kmp/core-data-experiment`, and it worked.** +5. ~~Which branch for the first KMP module?~~ **Decided: `kmp/core-data-experiment`, and it worked. + ** The fear was that converting `:core:data` to `kotlin("multiplatform")` would change how Gradle resolves it for the 13 dependent modules. It does not - a multiplatform module still publishes a - normal Android variant, and all 13 built unmodified. The branch was meant to be thrown away and is + normal Android variant, and all 13 built unmodified. The branch was meant to be thrown away and + is now worth merging. 6. **Still open: `wear/SmallestDoubleString.kt`** - the last `DecimalFormat` in a non-test file that is not the deliberate platform seam. It builds patterns from a runtime string, so it needs real logic rather than a mapping. -7. ~~The `:core:nssdk` converter switch.~~ **Done - see wave 7.** The joda parsing turned out **not** - to be part of the contract: it is a plain helper on `RemoteTreatment`, called after decoding, so it +7. ~~The `:core:nssdk` converter switch.~~ **Done - see wave 7.** The joda parsing turned out **not + ** + to be part of the contract: it is a plain helper on `RemoteTreatment`, called after decoding, so + it moves on its own schedule. The ~8 non-null fields were the real answer to this question, and the - answer was "give them defaults" - see the trap in wave 7. `RemoteStatusResponse` was left strict on + answer was "give them defaults" - see the trap in wave 7. `RemoteStatusResponse` was left strict + on purpose: `v3/status` is AAPS's own call to a server it just authenticated against, and a reply missing those fields is not something to carry on from. 8. ~~Retrofit converter, or straight to Ktor?~~ **Decided: the converter first, and the reasoning in @@ -1076,21 +1271,27 @@ keeps the old contract on a public interface method. characterization tests could actually do their job on the serialization half. The throwaway dependency cost nothing in the end: Retrofit 3.0.0 ships an official `converter-kotlinx-serialization`, so it was one catalog line and no third party code. -9. ~~The OkHttp disk cache needs a `Context`.~~ **Resolved: there is no cache.** It existed only so a +9. ~~The OkHttp disk cache needs a `Context`.~~ **Resolved: there is no cache.** It existed only so + a revalidated GET could surface as a 304, which the paging workers used as their stop condition. - That is now expressed directly - stop when the cursor cannot advance - so the cache, the `Context` + That is now expressed directly - stop when the cursor cannot advance - so the cache, the + `Context` and the two `Cache` instances that shared one directory are all gone. See wave 8. 10. ~~R8 has never run.~~ **Not a risk: AAPS does not minify.** Both convention plugins (`android-app-dependencies` and `android-module-dependencies`) set `isMinifyEnabled = false` for the `release` build type, so R8 never shrinks or obfuscates and the usual "kotlinx.serialization needs keep rules" problem cannot occur here. A release build was run anyway: it succeeds, and all five generated `$$serializer` classes plus the Retrofit kotlinx - converter are present in the release dex. The `benchmark` variant (`initWith(release)` plus debug + converter are present in the release dex. The `benchmark` variant (`initWith(release)` plus + debug signing) installs and starts clean; its runtime sync check is still outstanding because the emulator lost DNS, which starved both apps equally. -11. **Still open: merge `kmp/core-data-experiment` into `dev`.** Waves 5 to 9 all live there. Nothing - argues against it any more - the branch was device-verified in mixed-version pairs at each step - - but it is a real merge of a wire format change and deserves its own decision. It gets riskier the +11. **Still open: merge `kmp/core-data-experiment` into `dev`.** Waves 5 to 9 all live there. + Nothing + argues against it any more - the branch was device-verified in mixed-version pairs at each + step - + but it is a real merge of a wire format change and deserves its own decision. It gets riskier + the longer it waits: `dev` has not moved yet, so the merge is still a fast-forward. 12. **Still open: client-control crypto on a non-JVM platform.** It sits in `jvmMain` and nothing in `commonMain` calls it, so no target needs it today. A **follower** never signs commands or @@ -1100,12 +1301,26 @@ keeps the old contract on a public interface method. 13. **Still open: does web ever matter?** It is the one target that changes the **API shape** rather than just the implementation - WebCrypto is async-only, so `sign()` and `wrap()` would have to become `suspend`. Cheap to decide now, expensive to retrofit. +14. ~~compose-resources, moko-resources, or something else for strings?~~ **Decided: `TextRef` now, + compose-resources later, per module.** The two are not alternatives. compose-resources is still + the destination - same `strings.xml`, same Crowdin - but it cannot go on the key classes today, + because `getString()` is `suspend` and there is no public locale override, which would cost the + English search index. `TextRef` is the seam that lets each module move on its own schedule + without touching call sites twice. See wave 10. +15. **Still open: collapse `IntPreferenceKey.entries` / `resolvedEntries`.** Same + resource-id-or-string pair, and `TextRef` is exactly the type for it, but it reaches into plugin + call sites so it was left out of the `:core:*` phase. +16. ~~`RileyLinkStringPreferenceKey.MacAddress` should be a `NonPreferenceKey`.~~ **Done - moved to + `RileyLinkStringKey`.** It is storage, not a preference: no title, no screen, written by the + pairing wizard. With it gone the `titleResId = 0` default came out of all 26 enums, so a + preference key without a title no longer compiles. See wave 10. Waves 1 to 4 are committed on `dev`. Waves 5 to 9 are committed on `kmp/core-data-experiment`, each verified against a live Nightscout before the next one started - waves 5 and 6 on WSA, waves 7 to 9 on an emulator running a new master against a **pre-KMP client**, so every step was checked in a -mixed-version pair rather than only against itself. The `FoodManagement` comma defect in section 10 -is found but not fixed. +mixed-version pair rather than only against itself. Wave 10 is on the same branch and is not on a +device yet - it is a compile time refactor with no wire format in it, so the risk is different in +kind from waves 5 to 9. The `FoodManagement` comma defect in section 10 is found but not fixed. --- diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 288f7853b18d..445a23d42a0a 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -25,7 +25,7 @@ android { dependencies { implementation(project(":core:data")) - implementation(project(":core:keys")) + api(project(":core:keys")) // Dependency Injection api(libs.com.google.dagger.android) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index 0ad70b593ba4..6d7e53135fd8 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.RawRes import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef interface ResourceHelper { @@ -19,6 +20,28 @@ interface ResourceHelper { fun gs(@StringRes id: Int, vararg args: Any?): String fun gq(@PluralsRes id: Int, quantity: Int, vararg args: Any?): String fun gsNotLocalised(@StringRes id: Int, vararg args: Any?): String + + /** + * Resolves a [TextRef] outside Compose. Inside Compose use + * `app.aaps.core.ui.compose.stringResource` instead. + * + * Every non-Compose reader of a preference title goes through here, which is the point: when a + * module later moves its strings out of `res/values`, only this one method has to learn about + * the new form of [TextRef.Res]. + */ + fun gs(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.Res -> + if (ref.args.isEmpty()) gs(ref.id) + else gs(ref.id, *ref.args.toTypedArray()) + } + + /** Same, but always in English - used to build the search index. */ + fun gsNotLocalised(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.Res -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) + } + @ColorInt fun gc(@ColorRes id: Int): Int fun gd(@DrawableRes id: Int): Drawable? fun gb(@BoolRes id: Int): Boolean diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt index f4fc24e9c397..4705df0c3b15 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt @@ -6,12 +6,13 @@ import app.aaps.core.keys.interfaces.PreferenceEnabledCondition import app.aaps.core.keys.interfaces.SyncChannel import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.SyncSpec +import app.aaps.core.keys.interfaces.TextRef enum class BooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.SWITCH, override val calculatedDefaultValue: Boolean = false, override val defaultedBySM: Boolean = false, @@ -258,4 +259,8 @@ enum class BooleanKey( SiteRotationManagePump("site_rotation_manage_pump", defaultValue = false, titleResId = R.string.pref_title_site_rotation_manage_pump, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), SiteRotationManageCgm("site_rotation_manage_cgm", defaultValue = false, titleResId = R.string.pref_title_site_rotation_manage_cgm, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt index 37a366146432..c49d41713345 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt @@ -5,14 +5,15 @@ import app.aaps.core.keys.interfaces.DoublePreferenceKey import app.aaps.core.keys.interfaces.SyncChannel import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.SyncSpec +import app.aaps.core.keys.interfaces.TextRef enum class DoubleKey( override val key: String, override val defaultValue: Double, override val min: Double, override val max: Double, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val defaultedBySM: Boolean = false, override val calculatedBySM: Boolean = false, @@ -200,8 +201,28 @@ enum class DoubleKey( unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ApsAutoIsfMin(key = "autoISF_min", defaultValue = 1.0, min = 0.3, max = 1.0, titleResId = R.string.pref_title_autoisf_min, summaryResId = R.string.openapsama_autoISF_min_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), - ApsAutoIsfMax(key = "autoISF_max", defaultValue = 1.0, min = 1.0, max = 3.0, titleResId = R.string.pref_title_autoisf_max, summaryResId = R.string.openapsama_autoISF_max_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsAutoIsfMin( + key = "autoISF_min", + defaultValue = 1.0, + min = 0.3, + max = 1.0, + titleResId = R.string.pref_title_autoisf_min, + summaryResId = R.string.openapsama_autoISF_min_summary, + defaultedBySM = true, + unitType = UnitType.DOUBLE, + sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) + ), + ApsAutoIsfMax( + key = "autoISF_max", + defaultValue = 1.0, + min = 1.0, + max = 3.0, + titleResId = R.string.pref_title_autoisf_max, + summaryResId = R.string.openapsama_autoISF_max_summary, + defaultedBySM = true, + unitType = UnitType.DOUBLE, + sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) + ), ApsAutoIsfBgAccelWeight( key = "bgAccel_ISF_weight", defaultValue = 0.0, @@ -257,8 +278,28 @@ enum class DoubleKey( unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ApsAutoIsfPpWeight(key = "pp_ISF_weight", defaultValue = 0.0, min = 0.0, max = 0.15, titleResId = R.string.pref_title_pp_weight, summaryResId = R.string.openapsama_pp_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_3, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), - ApsAutoIsfDuraWeight(key = "dura_ISF_weight", defaultValue = 0.0, min = 0.0, max = 3.0, titleResId = R.string.pref_title_dura_weight, summaryResId = R.string.openapsama_dura_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsAutoIsfPpWeight( + key = "pp_ISF_weight", + defaultValue = 0.0, + min = 0.0, + max = 0.15, + titleResId = R.string.pref_title_pp_weight, + summaryResId = R.string.openapsama_pp_ISF_weight_summary, + defaultedBySM = true, + unitType = UnitType.DOUBLE_3, + sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) + ), + ApsAutoIsfDuraWeight( + key = "dura_ISF_weight", + defaultValue = 0.0, + min = 0.0, + max = 3.0, + titleResId = R.string.pref_title_dura_weight, + summaryResId = R.string.openapsama_dura_ISF_weight_summary, + defaultedBySM = true, + unitType = UnitType.DOUBLE_2, + sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) + ), ApsAutoIsfSmbDeliveryRatio( key = "openapsama_smb_delivery_ratio", defaultValue = 0.5, @@ -304,4 +345,8 @@ enum class DoubleKey( sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt index 1e2e26f4c8da..32d7bf30e03f 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt @@ -7,14 +7,15 @@ import app.aaps.core.keys.interfaces.PreferenceEnabledCondition import app.aaps.core.keys.interfaces.SyncChannel import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.SyncSpec +import app.aaps.core.keys.interfaces.TextRef enum class IntKey( override val key: String, override val defaultValue: Int, override val min: Int, override val max: Int, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val defaultedBySM: Boolean = false, @@ -451,4 +452,8 @@ enum class IntKey( ), sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt index a847e837c19c..6b6303f9a6e1 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt @@ -2,6 +2,7 @@ package app.aaps.core.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef /** * Legacy IntentKey enum - keys have been migrated to module-specific key enums: @@ -15,8 +16,8 @@ import app.aaps.core.keys.interfaces.IntentPreferenceKey */ enum class IntentKey( override val key: String, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.CLICK, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -26,4 +27,12 @@ enum class IntentKey( override val negativeDependency: BooleanPreferenceKey? = null, override val hideParentScreenIfHidden: Boolean = false, override val exportable: Boolean = false -) : IntentPreferenceKey \ No newline at end of file +) : IntentPreferenceKey { + + // This enum has no constants (see the note above), but the interface still requires the + // properties. The `;` is what separates the - empty - constant list from the members. + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt index 78a79dce3c62..66f0de088a7c 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt @@ -1,19 +1,20 @@ package app.aaps.core.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey -import app.aaps.core.keys.interfaces.PreferenceEnabledCondition import app.aaps.core.keys.interfaces.ElementVisibility +import app.aaps.core.keys.interfaces.PreferenceEnabledCondition import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator import app.aaps.core.keys.interfaces.SyncChannel import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.SyncSpec +import app.aaps.core.keys.interfaces.TextRef enum class StringKey( override val key: String, override val defaultValue: String, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val defaultedBySM: Boolean = false, @@ -200,4 +201,8 @@ enum class StringKey( validator = StringValidator.minLength(17) ), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt index cec1956effa7..a2a30d4c7e2f 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt @@ -4,6 +4,7 @@ import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.SyncChannel import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.SyncSpec +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey enum class UnitDoubleKey( @@ -11,8 +12,8 @@ enum class UnitDoubleKey( override val defaultValue: Double, override val minMgdl: Int, override val maxMgdl: Int, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -38,4 +39,8 @@ enum class UnitDoubleKey( dependency = BooleanKey.ApsUseDynamicSensitivity, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ) -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt index bc7598fcef92..8c8a55a616ea 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt @@ -22,17 +22,16 @@ interface PreferenceKey : NonPreferenceKey, PreferenceItem { override val key: String /** - * String resource ID for preference title. - * Use ResourceHelper.gs(titleResId) for localized string. + * Preference title. + * Use `ResourceHelper.gs(title)` outside Compose, or `stringResource(title)` inside it. */ - val titleResId: Int + val title: TextRef /** - * String resource ID for preference summary/description. - * Use ResourceHelper.gs(summaryResId) for localized string. - * null means no summary. + * Preference summary/description. + * null means no summary - there is no "empty" sentinel any more. */ - val summaryResId: Int? + val summary: TextRef? get() = null /** diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt new file mode 100644 index 000000000000..d96e4b332007 --- /dev/null +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt @@ -0,0 +1,52 @@ +package app.aaps.core.keys.interfaces + +/** + * A reference to user visible text, without saying where that text comes from. + * + * Preference keys used to carry a bare `Int` resource id. That works only on Android, so it blocks + * this module from becoming multiplatform. A [TextRef] says "there is some text here" and leaves the + * question of how to find it to whoever draws the screen - `ResourceHelper.gs(ref)` outside Compose, + * `stringResource(ref)` inside it. + * + * ### Do not persist it + * + * A [TextRef] is meaningful **only inside one running process**. [Res.id] must never be written to + * preferences, to the database, to a Nightscout document, to a wear message or into a crash report + * as a number, because the same number means different things on different platforms and will change + * again when a module moves its strings to `commonMain`. Persist the preference `key` instead, which + * is a stable string. + * + * ### Why an Int and not a string name + * + * Resolving by name needs `Resources.getIdentifier()`, which is a reflective lookup that R8 cannot + * see - it would keep every string alive and silently return 0 for a typo. Keeping the Android + * resource id means the existing `R.string.x` references stay compile checked exactly as they are + * today. + */ +sealed interface TextRef { + + /** + * Text that lives in a resource table. + * + * [id] is deliberately opaque: + * - **positive** - an Android resource id from `R.string.*`, used directly. This is the only + * form that exists today. + * - **negative** - a token generated when a module moves its strings to + * `commonMain/composeResources`; the resolver turns it into an index into that module's + * platform table. Android resource ids are always positive (`0x7f……`), so the two forms can + * coexist and modules can migrate one at a time rather than all at once. + * + * [args] are format arguments, in the order the format string expects. They are not checked at + * compile time - no worse than `stringResource(id, a, b)` today, but the arguments now travel + * further from the format string, so a mismatch shows up when the text is built. + */ + data class Res(val id: Int, val args: List = emptyList()) : TextRef + + /** + * Text that is only known at run time - a scanned pump name, a wiki page title, a user label. + * + * This replaces the `titleResId = 0` and `titleResId = -1` sentinels that used to mean "there is + * no resource here", which callers had to remember to test for. + */ + data class Literal(val text: String) : TextRef +} diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt new file mode 100644 index 000000000000..ef529c42db34 --- /dev/null +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -0,0 +1,27 @@ +package app.aaps.core.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.aaps.core.keys.interfaces.TextRef + +/** + * Resolves a [TextRef] to text inside a Composable. + * + * Every preference screen funnels through this one function, which is the point: when a module later + * moves its strings to `commonMain/composeResources`, only this resolver learns about the new + * negative-token form of [TextRef.Res]. The ~18 call sites do not change again. + * + * On Android today [TextRef.Res.id] is always an ordinary `R.string` id, so this is a direct call + * through to the platform. + */ +@Composable +fun stringResource(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.Res -> + if (ref.args.isEmpty()) stringResource(ref.id) + else stringResource(ref.id, *ref.args.toTypedArray()) +} + +/** Same, for an optional reference - returns null so callers can keep using `?.let { }`. */ +@Composable +fun stringResourceOrNull(ref: TextRef?): String? = ref?.let { stringResource(it) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index ae1ebdf206af..a434d35edbd5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.decimalPlaces import app.aaps.core.keys.interfaces.DoublePreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.keys.rangeResId import app.aaps.core.keys.step @@ -21,11 +22,13 @@ import app.aaps.core.keys.unitLabelResId import app.aaps.core.keys.valueResId import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.stringResourceOrNull /** * Composable double preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses doubleKey.titleResId + * @param title Optional title override. If null, uses doubleKey.title * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions * * @see AdaptiveDoublePreferencePreview @@ -33,15 +36,12 @@ import app.aaps.core.ui.compose.LocalPreferences @Composable fun AdaptiveDoublePreferenceItem( doubleKey: DoublePreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, unit: String = "", visibilityContext: VisibilityContext? = null ) { val preferences = LocalPreferences.current - val effectiveTitleResId = if (titleResId != 0) titleResId else doubleKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: doubleKey.title val visibility = calculatePreferenceVisibility( preferenceKey = doubleKey, @@ -67,8 +67,7 @@ fun AdaptiveDoublePreferenceItem( val valueFormat = NumberFormat.withDecimals(decimalPlaces) // Get summary if available - val summaryResId = doubleKey.summaryResId - val summary = if (summaryResId != null && summaryResId != 0) stringResource(summaryResId) else null + val summary = stringResourceOrNull(doubleKey.summary) // Use slider if min/max range is specified (not default extreme values) // Note: Double.MIN_VALUE is smallest positive value, not most negative @@ -81,7 +80,7 @@ fun AdaptiveDoublePreferenceItem( .padding(theme.padding) ) { TextWithSyncBadge( - text = stringResource(effectiveTitleResId), + text = stringResource(effectiveTitle), key = doubleKey, style = theme.titleTextStyle, // Mirror Preference's disabled styling (the switch row greys the same way) since this @@ -108,7 +107,7 @@ fun AdaptiveDoublePreferenceItem( valueFormatResId = valueFormatResId, valueFormat = valueFormat, unitLabel = unitLabel, - dialogLabel = stringResource(effectiveTitleResId), + dialogLabel = stringResource(effectiveTitle), dialogSummary = summary, enabled = visibility.enabled ) @@ -123,7 +122,7 @@ fun AdaptiveDoublePreferenceItem( } TextFieldPreference( state = state, - title = { PreferenceTitleWithSyncBadge(effectiveTitleResId, doubleKey) }, + title = { PreferenceTitleWithSyncBadge(effectiveTitle, doubleKey) }, textToValue = { text -> text.toDoubleOrNull()?.coerceIn(doubleKey.min, doubleKey.max) }, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index 12bfdb2960a3..095ce4a1a3d9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -13,16 +13,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.keys.rangeResId import app.aaps.core.keys.unitLabelResId import app.aaps.core.keys.valueResId import app.aaps.core.ui.R +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.stringResourceOrNull /** * Composable int preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses intKey.titleResId + * @param title Optional title override. If null, uses intKey.title * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions * * @see AdaptiveIntPreferencePreview @@ -30,14 +33,11 @@ import app.aaps.core.ui.R @Composable fun AdaptiveIntPreferenceItem( intKey: IntPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, unit: String = "", visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intKey.title val visibility = calculatePreferenceVisibility( preferenceKey = intKey, @@ -60,8 +60,7 @@ fun AdaptiveIntPreferenceItem( val unitLabel = unitLabelResId?.let { stringResource(it) } ?: unit // Get summary if available - val summaryResId = intKey.summaryResId - val summary = if (summaryResId != null && summaryResId != 0) stringResource(summaryResId) else null + val summary = stringResourceOrNull(intKey.summary) // Use slider if min/max range is specified (not default extreme values) val hasValidRange = intKey.min > Int.MIN_VALUE && intKey.max < Int.MAX_VALUE @@ -73,7 +72,7 @@ fun AdaptiveIntPreferenceItem( .padding(theme.padding) ) { TextWithSyncBadge( - text = stringResource(effectiveTitleResId), + text = stringResource(effectiveTitle), key = intKey, style = theme.titleTextStyle, // Mirror Preference's disabled styling (the switch row greys the same way) since this @@ -101,7 +100,7 @@ fun AdaptiveIntPreferenceItem( formatAsInt = true, valueFormat = NumberFormat.INTEGER, unitLabel = unitLabel, - dialogLabel = stringResource(effectiveTitleResId), + dialogLabel = stringResource(effectiveTitle), dialogSummary = summary, enabled = visibility.enabled ) @@ -116,7 +115,7 @@ fun AdaptiveIntPreferenceItem( } TextFieldPreference( state = state, - title = { PreferenceTitleWithSyncBadge(effectiveTitleResId, intKey) }, + title = { PreferenceTitleWithSyncBadge(effectiveTitle, intKey) }, textToValue = { text -> text.toIntOrNull()?.coerceIn(intKey.min, intKey.max) }, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index 5cdea2ee032a..692b693d0503 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -15,30 +15,29 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.compose.ComposeScreenContent import app.aaps.core.ui.compose.dialogs.OkCancelDialog +import app.aaps.core.ui.compose.stringResource /** * Composable intent preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses intentKey.titleResId - * @param summaryResId Optional summary resource ID. If null, uses intentKey.summaryResId + * @param title Optional title override. If null, uses intentKey.title + * @param summary Optional summary override. If null, uses intentKey.summary * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions */ @Composable fun AdaptiveIntentPreferenceItem( intentKey: IntentPreferenceKey, - titleResId: Int = 0, - summaryResId: Int? = null, + title: TextRef? = null, + summary: TextRef? = null, onClick: () -> Unit, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intentKey.titleResId - val effectiveSummaryResId = summaryResId ?: intentKey.summaryResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intentKey.title + val effectiveSummary = summary ?: intentKey.summary val visibility = calculateIntentPreferenceVisibility( intentKey = intentKey, @@ -53,7 +52,7 @@ fun AdaptiveIntentPreferenceItem( if (showConfirmation && confirmationResId != null) { OkCancelDialog( - title = stringResource(effectiveTitleResId), + title = stringResource(effectiveTitle), message = stringResource(confirmationResId), onConfirm = { onClick() @@ -70,8 +69,8 @@ fun AdaptiveIntentPreferenceItem( } Preference( - title = { Text(stringResource(effectiveTitleResId)) }, - summary = effectiveSummaryResId?.let { { Text(stringResource(it)) } }, + title = { Text(stringResource(effectiveTitle)) }, + summary = effectiveSummary?.let { { Text(stringResource(it)) } }, enabled = visibility.enabled, onClick = if (visibility.enabled) effectiveOnClick else null ) @@ -80,19 +79,16 @@ fun AdaptiveIntentPreferenceItem( /** * Composable URL preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses intentKey.titleResId + * @param title Optional title override. If null, uses intentKey.title */ @Composable fun AdaptiveUrlPreferenceItem( intentKey: IntentPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, url: String, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intentKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intentKey.title val visibility = calculateIntentPreferenceVisibility( intentKey = intentKey, @@ -103,7 +99,7 @@ fun AdaptiveUrlPreferenceItem( val uriHandler = LocalUriHandler.current Preference( - title = { Text(stringResource(effectiveTitleResId)) }, + title = { Text(stringResource(effectiveTitle)) }, summary = { Text(url) }, enabled = visibility.enabled, onClick = if (visibility.enabled) { @@ -115,22 +111,19 @@ fun AdaptiveUrlPreferenceItem( /** * Composable dynamic activity preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses intentKey.titleResId - * @param summaryResId Optional summary resource ID. If null, uses intentKey.summaryResId + * @param title Optional title override. If null, uses intentKey.title + * @param summary Optional summary override. If null, uses intentKey.summary */ @Composable fun AdaptiveDynamicActivityPreferenceItem( intentKey: IntentPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, activityClass: Class<*>, - summaryResId: Int? = null, + summary: TextRef? = null, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intentKey.titleResId - val effectiveSummaryResId = summaryResId ?: intentKey.summaryResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intentKey.title + val effectiveSummary = summary ?: intentKey.summary val visibility = calculateIntentPreferenceVisibility( intentKey = intentKey, @@ -141,8 +134,8 @@ fun AdaptiveDynamicActivityPreferenceItem( val context = LocalContext.current Preference( - title = { Text(stringResource(effectiveTitleResId)) }, - summary = effectiveSummaryResId?.let { { Text(stringResource(it)) } }, + title = { Text(stringResource(effectiveTitle)) }, + summary = effectiveSummary?.let { { Text(stringResource(it)) } }, enabled = visibility.enabled, onClick = if (visibility.enabled) { { context.startActivity(Intent(context, activityClass)) } @@ -159,14 +152,12 @@ fun AdaptiveComposeScreenPreferenceItem( intentKey: IntentPreferenceKey, composeScreen: ComposeScreenContent, onNavigate: (ComposeScreenContent) -> Unit, - titleResId: Int = 0, - summaryResId: Int? = null, + title: TextRef? = null, + summary: TextRef? = null, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intentKey.titleResId - val effectiveSummaryResId = summaryResId ?: intentKey.summaryResId - - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intentKey.title + val effectiveSummary = summary ?: intentKey.summary val visibility = calculateIntentPreferenceVisibility( intentKey = intentKey, @@ -176,8 +167,8 @@ fun AdaptiveComposeScreenPreferenceItem( if (!visibility.visible) return Preference( - title = { Text(stringResource(effectiveTitleResId)) }, - summary = effectiveSummaryResId?.let { { Text(stringResource(it)) } }, + title = { Text(stringResource(effectiveTitle)) }, + summary = effectiveSummary?.let { { Text(stringResource(it)) } }, enabled = visibility.enabled, onClick = if (visibility.enabled) { { onNavigate(composeScreen) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt index 61c5413e3386..599836dd8a92 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt @@ -6,16 +6,18 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.stringResourceOrNull /** * Composable list int preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses intKey.titleResId + * @param title Optional title override. If null, uses intKey.title * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions * * @see AdaptiveListIntPreferencePreview @@ -23,15 +25,12 @@ import app.aaps.core.keys.interfaces.VisibilityContext @Composable fun AdaptiveListIntPreferenceItem( intKey: IntPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, entries: List, entryValues: List, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else intKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: intKey.title val visibility = calculatePreferenceVisibility( preferenceKey = intKey, @@ -47,13 +46,12 @@ fun AdaptiveListIntPreferenceItem( val currentEntry = entries.getOrElse(currentIndex) { currentValue.toString() } // Get dialog summary from key - val summaryResId = intKey.summaryResId - val dialogSummary = if (summaryResId != null && summaryResId != 0) stringResource(summaryResId) else null + val dialogSummary = stringResourceOrNull(intKey.summary) ListPreference( state = state, values = entryValues, - title = { Text(stringResource(effectiveTitleResId)) }, + title = { Text(stringResource(effectiveTitle)) }, enabled = visibility.enabled, summary = { Text(currentEntry) }, dialogSummary = dialogSummary, @@ -67,20 +65,17 @@ fun AdaptiveListIntPreferenceItem( /** * Composable string list preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses stringKey.titleResId + * @param title Optional title override. If null, uses stringKey.title * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions */ @Composable fun AdaptiveStringListPreferenceItem( stringKey: StringPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, entries: Map, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else stringKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: stringKey.title val visibility = calculatePreferenceVisibility( preferenceKey = stringKey, @@ -95,13 +90,12 @@ fun AdaptiveStringListPreferenceItem( val values = entries.keys.toList() // Get dialog summary from key - val summaryResId = stringKey.summaryResId - val dialogSummary = if (summaryResId != null && summaryResId != 0) stringResource(summaryResId) else null + val dialogSummary = stringResourceOrNull(stringKey.summary) ListPreference( state = state, values = values, - title = { PreferenceTitleWithSyncBadge(effectiveTitleResId, stringKey) }, + title = { PreferenceTitleWithSyncBadge(effectiveTitle, stringKey) }, enabled = visibility.enabled, summary = { Text(currentEntry) }, dialogSummary = dialogSummary, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt index 3d09d0e37d06..218f8f6cd595 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt @@ -13,10 +13,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.dialogs.SetPasswordDialog +import app.aaps.core.ui.compose.stringResource /** * Composable password/PIN preference that opens a dialog to set the password. @@ -25,7 +27,7 @@ import app.aaps.core.ui.compose.dialogs.SetPasswordDialog * @param preferences The Preferences instance * @param stringKey The StringPreferenceKey (should have isPassword=true or isPin=true) * @param hashPassword Function to hash the password before storing - * @param titleResId Optional title resource ID. If 0, uses stringKey.titleResId + * @param titleResId Optional title resource ID. If 0, uses stringKey.title * @param visibilityKey Optional IntPreferenceKey that controls visibility * @param visibilityValue The value that visibilityKey must equal for this preference to be visible * @param visibilityContext Optional context for evaluating runtime visibility conditions @@ -37,16 +39,13 @@ fun AdaptivePasswordPreferenceItem( stringKey: StringPreferenceKey, hashPassword: (String) -> String, onShowMessage: (String) -> Unit, - titleResId: Int = 0, + title: TextRef? = null, visibilityKey: IntPreferenceKey? = null, visibilityValue: Int? = null, visibilityContext: VisibilityContext? = null ) { val preferences = LocalPreferences.current - val effectiveTitleResId = if (titleResId != 0) titleResId else stringKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: stringKey.title // Check conditional visibility based on visibilityKey if (visibilityKey != null && visibilityValue != null) { @@ -76,7 +75,7 @@ fun AdaptivePasswordPreferenceItem( } Preference( - title = { Text(stringResource(effectiveTitleResId)) }, + title = { Text(stringResource(effectiveTitle)) }, summary = { Text(summary) }, enabled = visibility.enabled, onClick = if (visibility.enabled) { @@ -91,7 +90,7 @@ fun AdaptivePasswordPreferenceItem( val notChangedMsg = stringResource(if (isPin) R.string.pin_not_changed else R.string.password_not_changed) SetPasswordDialog( - title = stringResource(effectiveTitleResId), + title = stringResource(effectiveTitle), pinInput = isPin, onConfirm = { password1, password2 -> when { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index 24d4827ad659..5cd688e55298 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource - import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.BooleanPreferenceKey @@ -18,11 +17,12 @@ import app.aaps.core.keys.interfaces.DoublePreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey import app.aaps.core.keys.interfaces.PreferenceKey -import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.keys.interfaces.StringKeyWithEntriesProvider import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey +import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.compose.ComposeScreenContent +import app.aaps.core.ui.compose.stringResource /** * Renders a preference based on its PreferenceKey type and preferenceType. @@ -120,7 +120,7 @@ fun AdaptivePreferenceItem( } else if (emptyMessageResId != null) { // Show disabled preference with empty message Preference( - title = { Text(stringResource(key.titleResId)) }, + title = { Text(stringResource(key.title)) }, summary = { Text(stringResource(emptyMessageResId)) }, enabled = false ) @@ -201,7 +201,7 @@ fun AdaptivePreferenceItem( val resolvedUrl = key.runtimeUrl ?: intentUrl ?: key.urlResId?.let { stringResource(it) } when { - resolvedClick != null -> { + resolvedClick != null -> { AdaptiveIntentPreferenceItem( intentKey = key, @@ -210,7 +210,7 @@ fun AdaptivePreferenceItem( ) } - resolvedCompose != null && onNavigateToCompose != null -> { + resolvedCompose != null && onNavigateToCompose != null -> { AdaptiveComposeScreenPreferenceItem( intentKey = key, composeScreen = resolvedCompose, @@ -219,7 +219,7 @@ fun AdaptivePreferenceItem( ) } - resolvedActivity != null -> { + resolvedActivity != null -> { AdaptiveDynamicActivityPreferenceItem( intentKey = key, @@ -228,7 +228,7 @@ fun AdaptivePreferenceItem( ) } - resolvedUrl != null -> { + resolvedUrl != null -> { AdaptiveUrlPreferenceItem( intentKey = key, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt index be0b5411a4a4..e0941f256135 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt @@ -6,7 +6,6 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable - import app.aaps.core.keys.interfaces.PreferenceItem import app.aaps.core.keys.interfaces.PreferenceKey import app.aaps.core.keys.interfaces.VisibilityContext diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index aa592380007f..b3827faea6fb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -11,17 +11,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext +import app.aaps.core.ui.R +import app.aaps.core.ui.compose.stringResource /** * Composable string preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses stringKey.titleResId - * @param summaryResId Optional summary resource ID. If null, uses stringKey.summaryResId + * @param title Optional title override. If null, uses stringKey.title + * @param summary Optional summary override. If null, uses stringKey.summary * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions * * @see AdaptiveStringPreferencePreview @@ -29,16 +31,13 @@ import app.aaps.core.keys.interfaces.VisibilityContext @Composable fun AdaptiveStringPreferenceItem( stringKey: StringPreferenceKey, - titleResId: Int = 0, - summaryResId: Int? = null, + title: TextRef? = null, + summary: TextRef? = null, isPassword: Boolean = false, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else stringKey.titleResId - val effectiveSummaryResId = summaryResId ?: stringKey.summaryResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: stringKey.title + val effectiveSummary = summary ?: stringKey.summary val visibility = calculatePreferenceVisibility( preferenceKey = stringKey, @@ -53,11 +52,11 @@ fun AdaptiveStringPreferenceItem( val isSecure = isPassword || stringKey.isPassword || stringKey.isPin // Get dialog summary from key - val dialogSummary = if (effectiveSummaryResId != null) stringResource(effectiveSummaryResId) else null + val dialogSummary = if (effectiveSummary != null) stringResource(effectiveSummary) else null TextFieldPreference( state = state, - title = { Text(stringResource(effectiveTitleResId)) }, + title = { Text(stringResource(effectiveTitle)) }, textToValue = { text -> val result = validator.validate(text) if (result.isValid) text else null @@ -69,16 +68,16 @@ fun AdaptiveStringPreferenceItem( } isSecure && value.isEmpty() -> { - val notSetResId = if (stringKey.isPin) app.aaps.core.ui.R.string.pin_not_set else app.aaps.core.ui.R.string.password_not_set - { Text(stringResource(effectiveSummaryResId ?: notSetResId)) } + val notSetResId = if (stringKey.isPin) R.string.pin_not_set else R.string.password_not_set + { Text(stringResource(effectiveSummary ?: TextRef.Res(notSetResId))) } } value.isNotEmpty() -> { { Text(value) } } - effectiveSummaryResId != null -> { - { Text(stringResource(effectiveSummaryResId)) } + effectiveSummary != null -> { + { Text(stringResource(effectiveSummary)) } } else -> null diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt index df6084899581..b1307c27cf1a 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt @@ -13,15 +13,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.BooleanKeyWithChangeGuard import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.R import app.aaps.core.ui.compose.dialogs.OkDialog +import app.aaps.core.ui.compose.stringResource /** * Composable switch preference for use inside card sections. * - * @param titleResId Optional title resource ID. If 0 or not provided, uses booleanKey.titleResId - * @param summaryResId Optional summary resource ID. If null, uses booleanKey.summaryResId + * @param title Optional title override. If null, uses booleanKey.title + * @param summary Optional summary override. If null, uses booleanKey.summary * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions * * @see AdaptiveSwitchPreferencePreview @@ -29,17 +31,14 @@ import app.aaps.core.ui.compose.dialogs.OkDialog @Composable fun AdaptiveSwitchPreferenceItem( booleanKey: BooleanPreferenceKey, - titleResId: Int = 0, - summaryResId: Int? = null, + title: TextRef? = null, + summary: TextRef? = null, summaryOnResId: Int? = null, summaryOffResId: Int? = null, visibilityContext: VisibilityContext? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else booleanKey.titleResId - val effectiveSummaryResId = summaryResId ?: booleanKey.summaryResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: booleanKey.title + val effectiveSummary = summary ?: booleanKey.summary val visibility = calculatePreferenceVisibility( preferenceKey = booleanKey, @@ -59,8 +58,8 @@ fun AdaptiveSwitchPreferenceItem( { Text(stringResource(if (state.value) summaryOnResId else summaryOffResId)) } } - effectiveSummaryResId != null -> { - { Text(stringResource(effectiveSummaryResId)) } + effectiveSummary != null -> { + { Text(stringResource(effectiveSummary)) } } else -> null @@ -77,14 +76,14 @@ fun AdaptiveSwitchPreferenceItem( guardMessage = message } }, - title = { PreferenceTitleWithSyncBadge(effectiveTitleResId, booleanKey) }, + title = { PreferenceTitleWithSyncBadge(effectiveTitle, booleanKey) }, summary = summary, enabled = visibility.enabled ) } else { SwitchPreference( state = state, - title = { PreferenceTitleWithSyncBadge(effectiveTitleResId, booleanKey) }, + title = { PreferenceTitleWithSyncBadge(effectiveTitle, booleanKey) }, summary = summary, enabled = visibility.enabled ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index 402e0f2499c0..0c66e0f56058 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -12,10 +12,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat -import app.aaps.core.keys.interfaces.VisibilityContext +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey +import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.LocalProfileUtil +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.stringResourceOrNull import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.abs @@ -25,21 +28,18 @@ import app.aaps.core.ui.R as UiR * Composable unit double preference for use inside card sections. * Handles glucose unit conversion (mg/dL <-> mmol/L). * - * @param titleResId Optional title resource ID. If 0 or not provided, uses unitKey.titleResId + * @param title Optional title override. If null, uses unitKey.title * @param visibilityContext Optional context for evaluating runtime visibility/enabled conditions */ @Composable fun AdaptiveUnitDoublePreferenceItem( unitKey: UnitDoublePreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, visibilityContext: VisibilityContext? = null ) { val preferences = LocalPreferences.current val profileUtil = LocalProfileUtil.current - val effectiveTitleResId = if (titleResId != 0) titleResId else unitKey.titleResId - - // Skip if no title resource is available - if (effectiveTitleResId == 0) return + val effectiveTitle = title ?: unitKey.title val visibility = calculatePreferenceVisibility( preferenceKey = unitKey, @@ -67,8 +67,7 @@ fun AdaptiveUnitDoublePreferenceItem( val unitLabel = stringResource(if (isMgdl) UiR.string.mgdl else UiR.string.mmol) // Get summary if available - val summaryResId = unitKey.summaryResId - val summary = if (summaryResId != null && summaryResId != 0) stringResource(summaryResId) else null + val summary = stringResourceOrNull(unitKey.summary) // Parse current display value to Double val currentValue = state.displayValue.toDoubleOrNull() ?: minDisplay @@ -79,7 +78,7 @@ fun AdaptiveUnitDoublePreferenceItem( .padding(theme.padding) ) { TextWithSyncBadge( - text = stringResource(effectiveTitleResId), + text = stringResource(effectiveTitle), key = unitKey, style = theme.titleTextStyle, // Mirror Preference's disabled styling (the switch row greys the same way) since this @@ -107,7 +106,7 @@ fun AdaptiveUnitDoublePreferenceItem( showValue = true, valueFormat = valueFormat, unitLabel = unitLabel, - dialogLabel = stringResource(effectiveTitleResId), + dialogLabel = stringResource(effectiveTitle), dialogSummary = summary, enabled = visibility.enabled ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt index 251bd2c8a325..a12e07223780 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt @@ -43,6 +43,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.compose.stringResource /** * Internal composable for clickable category header with expand/collapse icon. @@ -52,8 +54,8 @@ import androidx.compose.ui.unit.dp */ @Composable internal fun ClickablePreferenceCategoryHeader( - titleResId: Int, - summaryItems: List = emptyList(), + title: TextRef, + summaryItems: List = emptyList(), expanded: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier, @@ -67,7 +69,7 @@ internal fun ClickablePreferenceCategoryHeader( label = "expandIconRotation" ) - // Build summary text from list of resource IDs — resolve each string in composable context + // Build summary text from the child titles — resolve each one in composable context val resolvedSummaries = summaryItems.map { stringResource(it) } val summaryText = if (resolvedSummaries.isNotEmpty()) { resolvedSummaries.joinToString(", ") @@ -107,7 +109,7 @@ internal fun ClickablePreferenceCategoryHeader( } Column(modifier = Modifier.weight(1f)) { ProvideTextStyle(value = theme.categoryTextStyle) { - Text(text = stringResource(titleResId)) + Text(text = stringResource(title)) } // Show summary when collapsed if (!expanded && summaryText != null) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt index 7f61b935a980..86c2f6a39cd4 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import app.aaps.core.keys.interfaces.TextRef /** * Composable for a collapsible card section. @@ -39,8 +40,8 @@ import androidx.compose.ui.graphics.vector.ImageVector */ @Composable fun CollapsibleCardSectionContent( - titleResId: Int, - summaryItems: List = emptyList(), + title: TextRef, + summaryItems: List = emptyList(), expanded: Boolean, onToggle: () -> Unit, icon: ImageVector? = null, @@ -59,7 +60,7 @@ fun CollapsibleCardSectionContent( ) { Column { ClickablePreferenceCategoryHeader( - titleResId = titleResId, + title = title, summaryItems = summaryItems, expanded = expanded, onToggle = onToggle, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt index 32aea761d735..fc1b9b59cbf3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R @Preview(showBackground = true) @@ -13,7 +14,7 @@ import app.aaps.core.ui.R internal fun CollapsibleCardSectionContentPreview() { PreviewTheme { CollapsibleCardSectionContent( - titleResId = R.string.configbuilder_insulin, + title = TextRef.Res(R.string.configbuilder_insulin), expanded = true, onToggle = {} ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt index 55d17e6c4cc6..e4fb839efc8a 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt @@ -14,11 +14,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsSpacing +import app.aaps.core.ui.compose.stringResource /** * Inline text field for string preferences — for use in wizards and forms @@ -29,9 +30,9 @@ import app.aaps.core.ui.compose.AapsSpacing @Composable fun InlineStringPreferenceItem( stringKey: StringPreferenceKey, - titleResId: Int = 0 + title: TextRef? = null ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else stringKey.titleResId + val effectiveTitle = title ?: stringKey.title val state = rememberPreferenceStringState(stringKey) val validator = stringKey.validator val text = state.value @@ -46,9 +47,7 @@ fun InlineStringPreferenceItem( onValueChange = { newValue -> state.value = newValue }, - label = if (effectiveTitleResId != 0) { - { Text(stringResource(effectiveTitleResId)) } - } else null, + label = { Text(stringResource(effectiveTitle)) }, isError = !validationResult.isValid, supportingText = if (!validationResult.isValid) { { Text(validationResult.errorMessage ?: "") } @@ -67,19 +66,17 @@ fun InlineStringPreferenceItem( @Composable fun InlineStringListPreferenceItem( stringKey: StringPreferenceKey, - titleResId: Int = 0, + title: TextRef? = null, entries: Map ) { - val effectiveTitleResId = if (titleResId != 0) titleResId else stringKey.titleResId + val effectiveTitle = title ?: stringKey.title val state = rememberPreferenceStringState(stringKey) val selectedValue = state.value - if (effectiveTitleResId != 0) { - Text( - text = stringResource(effectiveTitleResId), - style = MaterialTheme.typography.titleMedium - ) - } + Text( + text = stringResource(effectiveTitle), + style = MaterialTheme.typography.titleMedium + ) Column(modifier = Modifier.selectableGroup()) { entries.forEach { (value, label) -> diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt index af0a9dbdee71..8fe1ad94575e 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt @@ -58,7 +58,7 @@ fun LazyListScope.addPreferenceSubScreenDef( // Get visibility context from CompositionLocal val visibilityContext = LocalVisibilityContext.current CollapsibleCardSectionContent( - titleResId = def.titleResId, + title = def.title, summaryItems = def.effectiveSummaryItems(), expanded = isExpanded, onToggle = { sectionState?.toggle(sectionKey, SectionLevel.TOP_LEVEL) }, @@ -112,7 +112,7 @@ private fun RenderPreferenceItems( // Header without card (no icon for nested subscreens) ClickablePreferenceCategoryHeader( - titleResId = item.titleResId, + title = item.title, summaryItems = item.effectiveSummaryItems(), expanded = isSubExpanded, onToggle = { sectionState?.toggle(subSectionKey, SectionLevel.SUB_SECTION, parentKey = parentKey) }, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt index be8bcaabcb87..0b662f99bfdc 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt @@ -32,7 +32,7 @@ fun PreferenceSheetContent( if (groups.isEmpty()) { // Single flat list → one always-expanded card titled by the def itself. CollapsibleCardSectionContent( - titleResId = settingsDef.titleResId, + title = settingsDef.title, expanded = true, onToggle = {}, icon = settingsDef.icon, @@ -47,7 +47,7 @@ fun PreferenceSheetContent( groups.forEach { group -> var expanded by remember(group.key) { mutableStateOf(false) } CollapsibleCardSectionContent( - titleResId = group.titleResId, + title = group.title, expanded = expanded, onToggle = { expanded = !expanded }, icon = group.icon diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt index 5cfa1a67d39a..8598c29c5c20 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt @@ -20,12 +20,12 @@ import app.aaps.core.keys.interfaces.IntNonPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey import app.aaps.core.keys.interfaces.PreferenceKey -import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringNonPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey +import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.compose.LocalConfig import app.aaps.core.ui.compose.LocalMasterReachable import app.aaps.core.ui.compose.LocalPreferences diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt index d5fae9279a2f..214ff3eab990 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt @@ -3,12 +3,17 @@ package app.aaps.core.ui.compose.preference import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.keys.interfaces.PreferenceItem import app.aaps.core.keys.interfaces.PreferenceKey +import app.aaps.core.keys.interfaces.TextRef /** * Lightweight preference subscreen definition. * Can contain both PreferenceKeys and nested PreferenceSubScreenDefs for hierarchical structure. * Content is auto-generated from items using AdaptivePreferenceList. * + * The constructor still takes plain resource ids, because roughly fifty plugin call sites build + * these with `titleResId = R.string.x`. The [title] and [summary] properties wrap them, so the + * rendering code only ever deals with [TextRef], the same as it does for [PreferenceKey]. + * * @param key Unique key for this subscreen * @param titleResId String resource ID for the screen title * @param items List of preference items (keys and/or nested subscreens) @@ -23,12 +28,18 @@ data class PreferenceSubScreenDef( val icon: ImageVector? = null ) : PreferenceItem { - /** Effective summary items - from items' titleResId */ - fun effectiveSummaryItems(): List = + /** Screen title, in the same form as [PreferenceKey.title]. */ + val title: TextRef = TextRef.Res(titleResId) + + /** Optional summary, in the same form as [PreferenceKey.summary]. */ + val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + + /** Titles of the contained items, used to build the summary line in the parent list. */ + fun effectiveSummaryItems(): List = items.mapNotNull { item -> when (item) { - is PreferenceKey -> item.titleResId.takeIf { it != 0 } - is PreferenceSubScreenDef -> item.titleResId.takeIf { it != 0 } + is PreferenceKey -> item.title + is PreferenceSubScreenDef -> item.title else -> null } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt index f1366792daa1..da3e6617c5d1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt @@ -22,8 +22,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import app.aaps.core.keys.interfaces.NonPreferenceKey import app.aaps.core.keys.interfaces.SyncDirection +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalConfig +import app.aaps.core.ui.compose.stringResource /** * Small badge marking something that two-way syncs with the master ("main phone"). @@ -97,5 +99,5 @@ fun TextWithSyncBadge( * on a client for Bidirectional keys. */ @Composable -fun PreferenceTitleWithSyncBadge(titleResId: Int, key: NonPreferenceKey?) = - TextWithSyncBadge(stringResource(titleResId), key) +fun PreferenceTitleWithSyncBadge(title: TextRef, key: NonPreferenceKey?) = + TextWithSyncBadge(stringResource(title), key) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt index 9353f65e9c6a..038d5d3faa51 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.keys.interfaces.PreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.navigation.descriptionResId import app.aaps.core.ui.compose.navigation.icon import app.aaps.core.ui.compose.navigation.labelResId @@ -21,9 +22,9 @@ sealed class SearchableItem { abstract val key: String /** - * Resource ID for the item's display title. + * The item's display title. */ - abstract val titleResId: Int + abstract val title: TextRef /** * Compose ImageVector icon. @@ -31,9 +32,9 @@ sealed class SearchableItem { open val icon: ImageVector? = null /** - * Optional resource ID for a summary/description. + * Optional summary/description. */ - open val summaryResId: Int? = null + open val summary: TextRef? = null /** * Optional reference to the plugin that owns this item. @@ -57,8 +58,8 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = preferenceKey.key - override val titleResId: Int = preferenceKey.titleResId - override val summaryResId: Int? = preferenceKey.summaryResId + override val title: TextRef = preferenceKey.title + override val summary: TextRef? = preferenceKey.summary override val plugin: PluginBase? = ownerPlugin } @@ -75,8 +76,8 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = screenDef.key - override val titleResId: Int = screenDef.titleResId - override val summaryResId: Int? = screenDef.summaryResId + override val title: TextRef = screenDef.title + override val summary: TextRef? = screenDef.summary override val plugin: PluginBase? = ownerPlugin override val icon: ImageVector? = screenDef.icon } @@ -92,11 +93,11 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = elementType.name - override val titleResId: Int = elementType.labelResId() + override val title: TextRef = TextRef.Res(elementType.labelResId()) @Deprecated("use icon") override val icon: ImageVector = elementType.icon() - override val summaryResId: Int? = elementType.descriptionResId().takeIf { it != 0 } + override val summary: TextRef? = elementType.descriptionResId().takeIf { it != 0 }?.let { TextRef.Res(it) } } /** @@ -110,8 +111,8 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = pluginRef.javaClass.simpleName - override val titleResId: Int = pluginRef.pluginDescription.pluginName - override val summaryResId: Int? = pluginRef.pluginDescription.description.takeIf { it != -1 } + override val title: TextRef = TextRef.Res(pluginRef.pluginDescription.pluginName) + override val summary: TextRef? = pluginRef.pluginDescription.description.takeIf { it != -1 }?.let { TextRef.Res(it) } override val plugin: PluginBase = pluginRef } @@ -130,6 +131,13 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = url - override val titleResId: Int = 0 // not used — title is dynamic + + /** + * The title comes from the ReadTheDocs API, so it is plain text, not a resource. It used to + * be stored as resource id 0, which the index then turned into an empty string - wiki hits + * could not be found by their own title. + */ + override val title: TextRef = TextRef.Literal(wikiTitle) + override val summary: TextRef? = snippet?.let { TextRef.Literal(it) } } } diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt index d9660f27f700..c7a991a0e004 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt @@ -8,11 +8,11 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import app.aaps.core.interfaces.configuration.Config as AppConfig import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.keys.StringKey import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.LocalConfig import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.LocalProfileUtil @@ -24,6 +24,7 @@ import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode +import app.aaps.core.interfaces.configuration.Config as AppConfig import app.aaps.core.ui.R as CoreUiR /** Robolectric render tests for the remaining preference composables (adaptive variants + base widgets). */ @@ -111,7 +112,7 @@ class MorePreferenceComponentsTest { fun clickableCategoryHeaderRendersTitle() { render { ClickablePreferenceCategoryHeader( - titleResId = CoreUiR.string.treatments, + title = TextRef.Res(CoreUiR.string.treatments), expanded = false, onToggle = {} ) @@ -143,7 +144,7 @@ class MorePreferenceComponentsTest { fun collapsibleCardShowsContentWhenExpanded() { render { CollapsibleCardSectionContent( - titleResId = CoreUiR.string.treatments, + title = TextRef.Res(CoreUiR.string.treatments), expanded = true, onToggle = {}, content = { Text("cardbody") } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt index 5691011c8a3f..3301534ed65d 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt @@ -41,7 +41,7 @@ class HardLimitsImplTest : TestBase() { runTest { whenever(persistenceLayer.insertPumpTherapyEventIfNewByTimestamp(any(), any(), any(), any(), any(), any())).thenReturn(PersistenceLayer.TransactionResult()) } - whenever(rh.gs(any())).thenReturn("") + whenever(rh.gs(any())).thenReturn("") whenever(rh.gs(any(), any())).thenReturn("") } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt index 6a9c8de41c4e..cc10ce0dfcfb 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt @@ -3,12 +3,13 @@ package app.aaps.plugins.aps.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.aps.R enum class ApsIntentKey( override val key: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.URL, override val urlResId: Int? = null, override val defaultedBySM: Boolean = false, @@ -27,4 +28,8 @@ enum class ApsIntentKey( preferenceType = PreferenceType.URL, urlResId = R.string.openapsama_link_to_preference_json_doc ) + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt index 912c84cde970..5814b3587214 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.AdaptiveIntPreferenceItem import javax.inject.Inject @@ -28,7 +29,7 @@ class SWEditIntNumber @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHe override fun Compose() { AdaptiveIntPreferenceItem( intKey = preference as IntPreferenceKey, - titleResId = label ?: 0 + title = label?.let { TextRef.Res(it) } ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt index 75eadaf22717..1b73ad1588df 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.DoublePreferenceKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.AdaptiveDoublePreferenceItem import javax.inject.Inject @@ -28,7 +29,7 @@ class SWEditNumber @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelpe override fun Compose() { AdaptiveDoublePreferenceItem( doubleKey = preference as DoublePreferenceKey, - titleResId = label ?: 0 + title = label?.let { TextRef.Res(it) } ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt index c5ba2be6985c..1b5bbca10b44 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey import app.aaps.core.ui.compose.preference.AdaptiveUnitDoublePreferenceItem import javax.inject.Inject @@ -30,7 +31,7 @@ class SWEditNumberWithUnits @Inject constructor(aapsLogger: AAPSLogger, rh: Reso override fun Compose() { AdaptiveUnitDoublePreferenceItem( unitKey = preference as UnitDoublePreferenceKey, - titleResId = label ?: 0 + title = label?.let { TextRef.Res(it) } ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt index 38f118fd10c3..fe175792c33d 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.InlineStringPreferenceItem import javax.inject.Inject @@ -34,7 +35,7 @@ class SWEditString @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelpe override fun Compose() { InlineStringPreferenceItem( stringKey = preference as StringPreferenceKey, - titleResId = label ?: 0 + title = label?.let { TextRef.Res(it) } ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt index 18cfdb93c8cb..ff2b53ba7612 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.InlineStringPreferenceItem import javax.inject.Inject @@ -28,7 +29,7 @@ class SWEditUrl @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, override fun Compose() { InlineStringPreferenceItem( stringKey = preference as StringPreferenceKey, - titleResId = label ?: 0 + title = label?.let { TextRef.Res(it) } ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt index ca1d5891b100..47cac6c0cc0a 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt @@ -8,6 +8,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.InlineStringListPreferenceItem import javax.inject.Inject @@ -48,7 +49,7 @@ class SWRadioButton @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelp } InlineStringListPreferenceItem( stringKey = key, - titleResId = label ?: 0, + title = label?.let { TextRef.Res(it) }, entries = entries ) } diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt index 436f26f9de6c..feaab41b50b1 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt @@ -1,14 +1,15 @@ package app.aaps.plugins.source.instara import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.source.R // Instara plugin-local user-editable preference keys enum class InstaraBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val defaultedBySM: Boolean = false, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, @@ -28,4 +29,8 @@ enum class InstaraBooleanKey( summaryResId = R.string.pref_summary_instara_history_request, showInNsClientMode = false ) -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt index 2dcf62599557..520050608e5d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt @@ -1,12 +1,13 @@ package app.aaps.plugins.sync.garmin.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class GarminBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -20,4 +21,7 @@ enum class GarminBooleanKey( ) : BooleanPreferenceKey { LocalHttpServer("communication_http", false, titleResId = R.string.garmin_local_http_server, defaultedBySM = true, hideParentScreenIfHidden = true), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt index 17a38f47118b..1f2e32464a36 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.sync.garmin.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class GarminIntKey( @@ -9,7 +10,7 @@ enum class GarminIntKey( override val defaultValue: Int, override val min: Int, override val max: Int, - override val titleResId: Int = 0, + private val titleResId: Int, override val defaultedBySM: Boolean = false, override val calculatedDefaultValue: Boolean = false, override val showInApsMode: Boolean = true, @@ -23,4 +24,7 @@ enum class GarminIntKey( ) : IntPreferenceKey { LocalHttpPort("communication_http_port", 28891, 1001, 65535, dependency = GarminBooleanKey.LocalHttpServer, titleResId = R.string.garmin_local_http_server_port, defaultedBySM = true, hideParentScreenIfHidden = true), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt index 55525b5522a0..9de5d3a00dca 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt @@ -3,12 +3,13 @@ package app.aaps.plugins.sync.garmin.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class GarminStringKey( override val key: String, override val defaultValue: String, - override val titleResId: Int = 0, + private val titleResId: Int, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -23,4 +24,7 @@ enum class GarminStringKey( ) : StringPreferenceKey { RequestKey(key = "garmin_aaps_key", defaultValue = "", titleResId = R.string.garmin_request_key), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt index 804b530fe9c8..80492f218e7b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt @@ -4,12 +4,13 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class SmsIntentKey( override val key: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.ACTIVITY, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -26,4 +27,8 @@ enum class SmsIntentKey( titleResId = R.string.smscommunicator_tab_otp_label, dependency = BooleanKey.SmsAllowRemoteCommands ) + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt index b4fca7b69e9a..05f69bb00cee 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt @@ -1,13 +1,14 @@ package app.aaps.plugins.sync.tidepool.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class TidepoolBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -21,4 +22,8 @@ enum class TidepoolBooleanKey( ) : BooleanPreferenceKey { UseTestServers("tidepool_dev_servers", false, titleResId = R.string.title_tidepool_dev_servers, summaryResId = R.string.summary_tidepool_dev_servers), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt index 1d31f5d94953..0370aee68517 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt @@ -3,12 +3,13 @@ package app.aaps.plugins.sync.xdrip.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R enum class XdripIntentKey( override val key: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.CLICK, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -26,4 +27,8 @@ enum class XdripIntentKey( summaryResId = R.string.xdrip_local_broadcasts_summary, preferenceType = PreferenceType.CLICK ) + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt index d57748ab3f2b..7ea74aabe34a 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt @@ -1,12 +1,13 @@ package info.nightscout.pump.combov2.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import info.nightscout.pump.combov2.R enum class ComboBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -22,4 +23,7 @@ enum class ComboBooleanKey( AutomaticReservoirEntry("combov2_automatic_reservoir_entry", true, titleResId = R.string.combov2_automatic_reservoir_entry), AutomaticBatteryEntry("combov2_automatic_battery_entry", true, titleResId = R.string.combov2_automatic_battery_entry), VerboseLogging("combov2_verbose_logging", false, titleResId = R.string.combov2_verbose_logging), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt index 70c3cb7abb07..032d2553ae6a 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt @@ -2,12 +2,13 @@ package info.nightscout.pump.combov2.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import info.nightscout.pump.combov2.R enum class ComboIntKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, + private val titleResId: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val calculatedDefaultValue: Boolean = false, @@ -23,4 +24,7 @@ enum class ComboIntKey( ) : IntPreferenceKey { DiscoveryDuration("combov2_bt_discovery_duration", defaultValue = 300, titleResId = R.string.combov2_discovery_duration, min = 30, max = 300), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt index 6914fc8506ef..a9003af8e809 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.dana.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R enum class DanaBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -23,4 +24,8 @@ enum class DanaBooleanKey( UseExtended("danar_useextended", true, titleResId = R.string.danar_useextended_title, defaultedBySM = true), LogCannulaChange("rs_logcanulachange", true, titleResId = R.string.rs_logcanulachange_title, summaryResId = R.string.rs_logcanulachange_summary), LogInsulinChange("rs_loginsulinchange", true, titleResId = R.string.rs_loginsulinchange_title, summaryResId = R.string.rs_loginsulinchange_summary), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt index 16f05a42fdaa..be61f263bb46 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt @@ -3,12 +3,13 @@ package app.aaps.pump.dana.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R enum class DanaIntKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, + private val titleResId: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, @@ -36,4 +37,7 @@ enum class DanaIntKey( 2 to R.string.bolus_speed_60 ) ), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt index edd9e537782d..33a479b6a2b0 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt @@ -2,11 +2,12 @@ package app.aaps.pump.dana.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R enum class DanaIntentKey( override val key: String, - override val titleResId: Int = 0, + private val titleResId: Int, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -18,4 +19,7 @@ enum class DanaIntentKey( ) : IntentPreferenceKey { BtSelector(key = "dana_rs_bt_selector", titleResId = R.string.selectedpump) -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) +} diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt index c94d5eb83eb4..cd78ed586aea 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.diaconn.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.diaconn.R enum class DiaconnBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -25,4 +26,8 @@ enum class DiaconnBooleanKey( LogTubeChange("diaconn_g8_logtubechange", true, titleResId = R.string.diaconn_g8_logtubechange_title, summaryResId = R.string.diaconn_g8_logtubechange_summary), LogBatteryChange("diaconn_g8_logbatterychanges", true, titleResId = R.string.diaconn_g8_logbatterychange_title, summaryResId = R.string.diaconn_g8_logbatterychange_summary), SendLogsToCloud("diaconn_g8_cloudsend", true, titleResId = R.string.diaconn_g8_cloudsend_title, summaryResId = R.string.diaconn_g8_cloudsend_summary), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt index cb463fb4fcd1..b8419a8f2f8e 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt @@ -3,6 +3,7 @@ package app.aaps.pump.diaconn.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.diaconn.R enum class DiaconnIntKey( @@ -10,7 +11,7 @@ enum class DiaconnIntKey( override val defaultValue: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, - override val titleResId: Int = 0, + private val titleResId: Int, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, @@ -41,4 +42,7 @@ enum class DiaconnIntKey( 8 to R.string.bolus_speed_8 ) ), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt index 54349ffd7a46..64235b9b72df 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt @@ -2,11 +2,12 @@ package app.aaps.pump.diaconn.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntentPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.diaconn.R enum class DiaconnIntentKey( override val key: String, - override val titleResId: Int, + private val titleResId: Int, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -18,4 +19,7 @@ enum class DiaconnIntentKey( ) : IntentPreferenceKey { BtSelector(key = "diaconn_bt_selector", titleResId = R.string.selectedpump) -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) +} diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt index 0ed16891cc6d..7ff7992d359b 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt @@ -1,12 +1,13 @@ package app.aaps.pump.eopatch.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.eopatch.R enum class EopatchBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -20,4 +21,7 @@ enum class EopatchBooleanKey( ) : BooleanPreferenceKey { BuzzerReminder("eopatch_patch_buzzer_reminders", false, titleResId = R.string.patch_buzzer_reminders), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt index 9a69732f8051..f982e9524c89 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt @@ -3,12 +3,13 @@ package app.aaps.pump.eopatch.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.eopatch.R enum class EopatchIntKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, + private val titleResId: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, @@ -27,4 +28,7 @@ enum class EopatchIntKey( LowReservoirReminder("eopatch_low_reservoir_reminders", 10, titleResId = R.string.low_reservoir, preferenceType = PreferenceType.LIST), ExpirationReminder("eopatch_expiration_reminders", 4, titleResId = R.string.patch_expiration_reminders, preferenceType = PreferenceType.LIST), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt index 948abb36dfda..681428c5e144 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt @@ -1,12 +1,13 @@ package app.aaps.pump.equil.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.equil.R enum class EquilBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -21,4 +22,7 @@ enum class EquilBooleanPreferenceKey( EquilAlarmBattery("key_equil_alarm_battery", true, titleResId = R.string.equil_settings_alarm_battery), EquilAlarmInsulin("key_equil_alarm_insulin", true, titleResId = R.string.equil_settings_alarm_insulin), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt index 3e8bdc761ba8..7bc2a55b7acf 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt @@ -3,6 +3,7 @@ package app.aaps.pump.equil.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.equil.R enum class EquilIntPreferenceKey( @@ -10,7 +11,7 @@ enum class EquilIntPreferenceKey( override val defaultValue: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, - override val titleResId: Int = 0, + private val titleResId: Int, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, @@ -39,4 +40,7 @@ enum class EquilIntPreferenceKey( 3 to R.string.equil_tone_mode_tone_and_shake ) ), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt index 7a7feb7fac5d..605f56883fe3 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.insight.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.insight.R enum class InsightBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -29,4 +30,8 @@ enum class InsightBooleanKey( EnableTbrEmulation("insight_enable_tbr_emulation", false, titleResId = R.string.enable_tbr_emulation, summaryResId = R.string.enable_tbr_emulation_summary), DisableVibration("insight_disable_vibration", false, titleResId = R.string.disable_vibration, summaryResId = R.string.disable_vibration_summary), DisableVibrationAuto("insight_disable_vibration_auto", false, titleResId = R.string.disable_vibration_auto, summaryResId = R.string.disable_vibration_auto_summary), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt index fecad9c1848b..187918f3aa6b 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt @@ -2,12 +2,13 @@ package app.aaps.pump.insight.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.insight.R enum class InsightIntKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, + private val titleResId: Int, override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val calculatedDefaultValue: Boolean = false, @@ -25,4 +26,7 @@ enum class InsightIntKey( MinRecoveryDuration("insight_min_recovery_duration", 5, titleResId = R.string.min_recovery_duration), MaxRecoveryDuration("insight_max_recovery_duration", 20, titleResId = R.string.max_recovery_duration), DisconnectDelay("insight_disconnect_delay", 5, titleResId = R.string.disconnect_delay), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt index 9d425c1b0ea0..cc946893f718 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.medtronic.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtronic.R enum class MedtronicBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -26,4 +27,8 @@ enum class MedtronicBooleanPreferenceKey( titleResId = R.string.set_neutral_temps_title, summaryResId = R.string.set_neutral_temps_summary ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt index b7397c359d35..d9f54d6d1e8b 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt @@ -3,13 +3,14 @@ package app.aaps.pump.medtronic.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtronic.R enum class MedtronicIntPreferenceKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val min: Int = Int.MIN_VALUE, @@ -53,4 +54,8 @@ enum class MedtronicIntPreferenceKey( min = 5, max = 15 ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt index a5dd3b3cfb0f..ae096e0cae8c 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt @@ -4,13 +4,14 @@ import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtronic.R enum class MedtronicStringPreferenceKey( override val key: String, override val defaultValue: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val defaultedBySM: Boolean = false, @@ -76,4 +77,8 @@ enum class MedtronicStringPreferenceKey( app.aaps.pump.medtronic.defs.BatteryType.NiMH.key to R.string.medtronic_pump_battery_nimh ) ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicService.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicService.kt index cfda2e26dbe8..075f01c879e9 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicService.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicService.kt @@ -73,7 +73,7 @@ class RileyLinkMedtronicService : RileyLinkService() { setPumpIDString(preferences.get(MedtronicStringPreferenceKey.Serial)) // get most recently used RileyLink address and name - rileyLinkServiceData.rileyLinkAddress = preferences.get(RileyLinkStringPreferenceKey.MacAddress) + rileyLinkServiceData.rileyLinkAddress = preferences.get(RileyLinkStringKey.MacAddress) rileyLinkServiceData.rileyLinkName = preferences.get(RileyLinkStringKey.Name) rfSpy.startReader() aapsLogger.debug(LTag.PUMPCOMM, "RileyLinkMedtronicService newly constructed") @@ -158,7 +158,7 @@ class RileyLinkMedtronicService : RileyLinkService() { } } rileyLinkServiceData.rileyLinkTargetFrequency = RileyLinkTargetFrequency.getByKey(preferences.get(MedtronicStringPreferenceKey.PumpFrequency)) - val rileyLinkAddress = preferences.get(RileyLinkStringPreferenceKey.MacAddress) + val rileyLinkAddress = preferences.get(RileyLinkStringKey.MacAddress) if (rileyLinkAddress.isEmpty()) { aapsLogger.debug(LTag.PUMP, "RileyLink address invalid: null") medtronicPumpStatus.errorDescription = rh.gs(R.string.medtronic_error_rileylink_address_invalid) diff --git a/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicServiceUTest.kt b/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicServiceUTest.kt index 0e84db37b49a..516b6f2c7586 100644 --- a/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicServiceUTest.kt +++ b/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/service/RileyLinkMedtronicServiceUTest.kt @@ -52,7 +52,7 @@ class RileyLinkMedtronicServiceUTest : TestBaseWithProfile() { // Create service instance service = RileyLinkMedtronicService().also { - // Inject dependencies via reflection since it's a service with @Inject fields + // Inject dependencies via reflection since it's a service with @Inject fields it.medtronicPumpPlugin = medtronicPumpPlugin it.medtronicUtil = medtronicUtil it.medtronicPumpStatus = medtronicPumpStatus @@ -74,7 +74,7 @@ class RileyLinkMedtronicServiceUTest : TestBaseWithProfile() { whenever(preferences.get(MedtronicStringPreferenceKey.Serial)).thenReturn("123456") whenever(preferences.get(MedtronicStringPreferenceKey.PumpType)).thenReturn("522") whenever(preferences.get(MedtronicStringPreferenceKey.PumpFrequency)).thenReturn("medtronic_pump_frequency_us_ca") - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("AA:BB:CC:DD:EE:FF") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("AA:BB:CC:DD:EE:FF") whenever(preferences.get(RileyLinkStringKey.Name)).thenReturn("RileyLink") whenever(preferences.get(MedtronicIntPreferenceKey.MaxBolus)).thenReturn(10) whenever(preferences.get(MedtronicIntPreferenceKey.MaxBasal)).thenReturn(5) @@ -165,7 +165,7 @@ class RileyLinkMedtronicServiceUTest : TestBaseWithProfile() { @Test fun `test verifyConfiguration with empty RileyLink address returns false`() { setupValidConfiguration() - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("") whenever(rh.gs(R.string.medtronic_error_rileylink_address_invalid)).thenReturn("Invalid RileyLink address") @@ -178,7 +178,7 @@ class RileyLinkMedtronicServiceUTest : TestBaseWithProfile() { @Test fun `test verifyConfiguration with invalid RileyLink MAC address format returns false`() { setupValidConfiguration() - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("INVALID_MAC") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("INVALID_MAC") whenever(rh.gs(R.string.medtronic_error_rileylink_address_invalid)).thenReturn("Invalid RileyLink address") @@ -193,15 +193,15 @@ class RileyLinkMedtronicServiceUTest : TestBaseWithProfile() { setupValidConfiguration() // Test with colons - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("AA:BB:CC:DD:EE:FF") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("AA:BB:CC:DD:EE:FF") assertThat(service.verifyConfiguration(forceRileyLinkAddressRenewal = false)).isTrue() // Test lowercase - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("aa:bb:cc:dd:ee:ff") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("aa:bb:cc:dd:ee:ff") assertThat(service.verifyConfiguration(forceRileyLinkAddressRenewal = false)).isTrue() // Test single digit hex values - whenever(preferences.get(RileyLinkStringPreferenceKey.MacAddress)).thenReturn("0:1:2:3:4:5") + whenever(preferences.get(RileyLinkStringKey.MacAddress)).thenReturn("0:1:2:3:4:5") assertThat(service.verifyConfiguration(forceRileyLinkAddressRenewal = false)).isTrue() } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt index 7adab6540a5c..6ced33005ca3 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.medtrum.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtrum.R enum class MedtrumBooleanKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -38,4 +39,8 @@ enum class MedtrumBooleanKey( titleResId = R.string.scan_on_connection_error_title, summaryResId = R.string.scan_on_connection_error_summary ), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt index 4411a8b63b2f..3f95724bd29b 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt @@ -2,13 +2,14 @@ package app.aaps.pump.medtrum.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtrum.R enum class MedtrumIntKey( override val key: String, override val defaultValue: Int, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override var min: Int = Int.MIN_VALUE, override var max: Int = Int.MAX_VALUE, override val calculatedDefaultValue: Boolean = false, @@ -48,4 +49,8 @@ enum class MedtrumIntKey( min = 20, max = 180 ), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt index cafa70b50217..37033070bbef 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt @@ -5,13 +5,14 @@ import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.PreferenceEnabledCondition import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtrum.R enum class MedtrumStringKey( override val key: String, override val defaultValue: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val defaultedBySM: Boolean = false, @@ -45,4 +46,8 @@ enum class MedtrumStringKey( "7" to R.string.alarm_setting_silent ) ), + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt index bfee5cc1575d..55c8157d213f 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt @@ -1,12 +1,13 @@ package app.aaps.pump.omnipod.common.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.omnipod.common.R enum class DashBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -21,4 +22,7 @@ enum class DashBooleanPreferenceKey( SoundDeliverySuspendedNotification("AAPS.Omnipod.notification_delivery_suspended_sound_enabled", true, titleResId = R.string.omnipod_common_preferences_notification_delivery_suspended_sound_enabled), UseBonding("AAPS.Omnipod.Dash.use_bonding", false, titleResId = R.string.omnipod_dash_use_bonding), + ; + + override val title: TextRef = TextRef.Res(titleResId) } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt index 57d7decaaeaf..3a374560e941 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt @@ -2,13 +2,14 @@ package app.aaps.pump.omnipod.common.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.omnipod.common.R enum class OmnipodBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -42,4 +43,6 @@ enum class OmnipodBooleanPreferenceKey( AutomaticallyAcknowledgeAlerts("AAPS.Omnipod.automatically_acknowledge_alerts_enabled", false, titleResId = R.string.omnipod_common_preferences_automatically_silence_alerts); override val preferenceType: PreferenceType = PreferenceType.SWITCH + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt index 12f18b3f9aed..7119c7c6e433 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt @@ -3,6 +3,7 @@ package app.aaps.pump.omnipod.common.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.IntPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.omnipod.common.R enum class OmnipodIntPreferenceKey( @@ -10,8 +11,8 @@ enum class OmnipodIntPreferenceKey( override val min: Int, override val max: Int, override val defaultValue: Int, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val entries: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, @@ -42,4 +43,6 @@ enum class OmnipodIntPreferenceKey( ); override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt index 55bd37b1174b..a3c4b9941495 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt @@ -1,6 +1,7 @@ package app.aaps.pump.omnipod.eros.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.common.hw.rileylink.keys.RileylinkBooleanPreferenceKey import app.aaps.pump.omnipod.eros.R import app.aaps.pump.omnipod.common.R as CommonR @@ -8,7 +9,7 @@ import app.aaps.pump.omnipod.common.R as CommonR enum class ErosBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int, + private val titleResId: Int, override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -26,4 +27,7 @@ enum class ErosBooleanPreferenceKey( ShowPulseLogButton("AAPS.Omnipod.pulse_log_button_enabled", false, titleResId = R.string.omnipod_eros_preferences_pulse_log_button_enabled), ShowRileyLinkStatsButton("AAPS.Omnipod.rileylink_stats_button_enabled", false, titleResId = R.string.omnipod_eros_preferences_riley_link_stats_button_enabled), TimeChangeEnabled("AAPS.Omnipod.time_change_enabled", true, titleResId = CommonR.string.omnipod_common_preferences_time_change_enabled), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) +} diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/rileylink/service/RileyLinkOmnipodService.java b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/rileylink/service/RileyLinkOmnipodService.java index 4ba9356a96a0..1d77c3161f51 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/rileylink/service/RileyLinkOmnipodService.java +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/rileylink/service/RileyLinkOmnipodService.java @@ -20,7 +20,6 @@ import app.aaps.pump.common.hw.rileylink.ble.defs.RileyLinkTargetFrequency; import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice; import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringKey; -import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringPreferenceKey; import app.aaps.pump.common.hw.rileylink.service.RileyLinkService; import app.aaps.pump.omnipod.eros.OmnipodErosPumpPlugin; import app.aaps.pump.omnipod.eros.R; @@ -69,7 +68,7 @@ public void initRileyLinkServiceData() { rileyLinkServiceData.setTargetDevice(RileyLinkTargetDevice.Omnipod); rileyLinkServiceData.setRileyLinkTargetFrequency(RileyLinkTargetFrequency.Omnipod); - rileyLinkServiceData.setRileyLinkAddress(preferences.get(RileyLinkStringPreferenceKey.MacAddress)); + rileyLinkServiceData.setRileyLinkAddress(preferences.get(RileyLinkStringKey.MacAddress)); rileyLinkServiceData.setRileyLinkName(preferences.get(RileyLinkStringKey.Name)); rfSpy.startReader(); @@ -103,7 +102,7 @@ public boolean verifyConfiguration(boolean forceRileyLinkAddressRenewal) { try { errorDescription = null; - String rileyLinkAddress = preferences.get(RileyLinkStringPreferenceKey.MacAddress); + String rileyLinkAddress = preferences.get(RileyLinkStringKey.MacAddress); if (StringUtils.isEmpty(rileyLinkAddress)) { aapsLogger.debug(LTag.PUMPBTCOMM, "RileyLink address invalid: no address"); diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardViewModel.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardViewModel.kt index f4a651020a3f..37dd22389f64 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardViewModel.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardViewModel.kt @@ -20,7 +20,6 @@ import app.aaps.pump.common.hw.rileylink.RileyLinkUtil import app.aaps.pump.common.hw.rileylink.ble.data.GattAttributes import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpDevice import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringKey -import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringPreferenceKey import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow @@ -111,7 +110,7 @@ class RileyLinkPairWizardViewModel @Inject constructor( aapsLogger.debug(LTag.PUMPBTCOMM, "RileyLinkPairWizard: selected ${device.name} (${device.address})") stopScan() - preferences.put(RileyLinkStringPreferenceKey.MacAddress, device.address) + preferences.put(RileyLinkStringKey.MacAddress, device.address) preferences.put(RileyLinkStringKey.Name, device.name) // Force RL reconnection with new address diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringKey.kt index 17fd2e6786c0..149709f9c61a 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringKey.kt @@ -9,4 +9,5 @@ enum class RileyLinkStringKey( ) : StringNonPreferenceKey { Name("pref_rileylink_name", ""), -} \ No newline at end of file + MacAddress("pref_rileylink_mac_address", ""), +} diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt index 2fda027734f9..70118f59ec6c 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt @@ -3,13 +3,14 @@ package app.aaps.pump.common.hw.rileylink.keys import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.common.hw.rileylink.R enum class RileyLinkStringPreferenceKey( override val key: String, override val defaultValue: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val entries: Map = emptyMap(), override val defaultedBySM: Boolean = false, @@ -24,10 +25,6 @@ enum class RileyLinkStringPreferenceKey( override val exportable: Boolean = true ) : StringPreferenceKey { - MacAddress( - key = "pref_rileylink_mac_address", - defaultValue = "" - ), Encoding( key = "pref_medtronic_encoding", defaultValue = "medtronic_pump_encoding_4b6b_rileylink", @@ -38,4 +35,8 @@ enum class RileyLinkStringPreferenceKey( "medtronic_pump_encoding_4b6b_rileylink" to R.string.medtronic_pump_encoding_4b6b_rileylink ) ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt index 76e16835fb92..e0def9e949a0 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt @@ -1,13 +1,14 @@ package app.aaps.pump.common.hw.rileylink.keys import app.aaps.core.keys.interfaces.BooleanPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.common.hw.rileylink.R enum class RileylinkBooleanPreferenceKey( override val key: String, override val defaultValue: Boolean, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, + private val titleResId: Int, + private val summaryResId: Int? = null, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -32,4 +33,8 @@ enum class RileylinkBooleanPreferenceKey( titleResId = R.string.riley_link_show_battery_level, summaryResId = R.string.riley_link_show_battery_level_summary ), -} \ No newline at end of file + ; + + override val title: TextRef = TextRef.Res(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } +} diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/service/RileyLinkBroadcastReceiver.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/service/RileyLinkBroadcastReceiver.kt index 36c53bdff480..d1a99ab3403a 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/service/RileyLinkBroadcastReceiver.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/service/RileyLinkBroadcastReceiver.kt @@ -13,7 +13,7 @@ import app.aaps.pump.common.hw.rileylink.RileyLinkConst import app.aaps.pump.common.hw.rileylink.defs.RileyLinkError import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpDevice import app.aaps.pump.common.hw.rileylink.defs.RileyLinkServiceState -import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringPreferenceKey +import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringKey import app.aaps.pump.common.hw.rileylink.service.tasks.DiscoverGattServicesTask import app.aaps.pump.common.hw.rileylink.service.tasks.InitializePumpManagerTask import app.aaps.pump.common.hw.rileylink.service.tasks.ServiceTask @@ -119,7 +119,7 @@ class RileyLinkBroadcastReceiver : DaggerBroadcastReceiver() { } RileyLinkConst.Intents.RileyLinkNewAddressSet -> { - val rileylinkBLEAddress = preferences.get(RileyLinkStringPreferenceKey.MacAddress) + val rileylinkBLEAddress = preferences.get(RileyLinkStringKey.MacAddress) if (rileylinkBLEAddress == "") aapsLogger.error("No Rileylink BLE Address saved in app") else rileyLinkService?.reconfigureRileyLink(rileylinkBLEAddress) true diff --git a/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt b/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt index fe2c44c87f6d..a11028467bcb 100644 --- a/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt +++ b/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt @@ -6,6 +6,7 @@ import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.PreferenceKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.core.ui.search.SearchableItem import app.aaps.core.ui.search.SearchableProvider @@ -235,9 +236,6 @@ class SearchIndexBuilder @Inject constructor( val parentScreenMap = buildParentScreenMap() allKeys.forEach { prefKey -> - // Skip keys with invalid title resource - if (prefKey.titleResId == 0) return@forEach - // Skip keys not visible in the current build mode (mirror calculatePreferenceVisibility) if (preferences.apsMode && !prefKey.showInApsMode) return@forEach if (preferences.nsclientMode && !prefKey.showInNsClientMode) return@forEach @@ -314,10 +312,10 @@ class SearchIndexBuilder @Inject constructor( is SearchableItem.Wiki -> SearchCategory.WIKI } - val localizedTitle = safeGetString(item.titleResId) - val englishTitle = safeGetStringNotLocalised(item.titleResId) - val localizedSummary = item.summaryResId?.let { safeGetString(it) } - val englishSummary = item.summaryResId?.let { safeGetStringNotLocalised(it) } + val localizedTitle = safeGetString(item.title) + val englishTitle = safeGetStringNotLocalised(item.title) + val localizedSummary = item.summary?.let { safeGetString(it) } + val englishSummary = item.summary?.let { safeGetStringNotLocalised(it) } return SearchIndexEntry( item = item, @@ -329,17 +327,17 @@ class SearchIndexBuilder @Inject constructor( ) } - private fun safeGetString(resId: Int): String { + private fun safeGetString(ref: TextRef): String { return try { - if (resId != 0) rh.gs(resId) else "" + rh.gs(ref) } catch (_: Exception) { "" } } - private fun safeGetStringNotLocalised(resId: Int): String { + private fun safeGetStringNotLocalised(ref: TextRef): String { return try { - if (resId != 0) rh.gsNotLocalised(resId) else "" + rh.gsNotLocalised(ref) } catch (_: Exception) { "" } From defd131deca9cfe24525a5c56e73c142e9552393 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 7 Aug 2026 22:34:59 +0200 Subject: [PATCH 013/146] :core:keys Eliminate dependencies --- core/data/build.gradle.kts | 5 +- .../plugin/PluginBaseWithPreferences.kt | 4 +- .../core/interfaces/pump/PumpPluginBase.kt | 2 +- .../keys/interfaces/IntentPreferenceKey.kt | 28 -------- .../aaps/core/keys/interfaces/Preferences.kt | 11 +++- .../keys/interfaces/StringPreferenceKey.kt | 24 +++---- core/nssdk/build.gradle.kts | 8 +-- .../preference/AdaptiveIntentPreference.kt | 37 ----------- .../preference/AdaptivePreferenceItem.kt | 31 +++------ .../ui/compose/preference/PreviewUtils.kt | 2 +- .../alerts/LocalAlertUtilsImpl.kt | 2 +- .../sharedPreferences/PreferencesImpl.kt | 65 ++++++++----------- .../plugins/aps/autotune/AutotunePlugin.kt | 2 +- .../aps/openAPSAMA/OpenAPSAMAPlugin.kt | 2 +- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 2 +- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 2 +- plugins/calibration/build.gradle.kts | 1 - .../constraints/dstHelper/DstHelperPlugin.kt | 2 +- .../objectives/ObjectivesPlugin.kt | 2 +- .../SignatureVerifierPlugin.kt | 2 +- .../versionChecker/VersionCheckerPlugin.kt | 2 +- .../smoothing/UnscentedKalmanFilterPlugin.kt | 2 +- .../plugins/source/AbstractBgSourcePlugin.kt | 2 +- ...stractBgSourceWithSensorInsertLogPlugin.kt | 2 +- .../app/aaps/plugins/source/GlunovoPlugin.kt | 2 +- .../aaps/plugins/source/IntelligoPlugin.kt | 2 +- .../plugins/source/instara/InstaraPlugin.kt | 2 +- .../aaps/plugins/sync/garmin/GarminPlugin.kt | 2 +- .../sync/nsclientV3/NSClientV3Plugin.kt | 2 +- .../openhumans/OpenHumansUploaderPlugin.kt | 2 +- .../smsCommunicator/SmsCommunicatorPlugin.kt | 2 +- .../plugins/sync/tidepool/TidepoolPlugin.kt | 5 +- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 2 +- .../nightscout/pump/combov2/ComboV2Plugin.kt | 5 +- .../aaps/pump/common/PumpPluginAbstract.kt | 2 +- .../aaps/pump/danar/AbstractDanaRPlugin.kt | 2 +- .../app/aaps/pump/danars/DanaRSPlugin.kt | 3 +- .../app/aaps/pump/diaconn/DiaconnG8Plugin.kt | 6 +- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 4 +- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 5 +- .../app/aaps/pump/insight/InsightPlugin.kt | 5 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 9 +-- .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 42 ++++++------ .../omnipod/dash/OmnipodDashPumpPlugin.kt | 6 +- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 4 +- .../aaps/pump/virtual/VirtualPumpPlugin.kt | 2 +- .../wear/sharedPreferences/PreferencesImpl.kt | 52 +++++---------- .../sharedPreferences/PreferencesImplTest.kt | 6 +- 48 files changed, 145 insertions(+), 271 deletions(-) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index bee18f014f99..c458a095d4c9 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -26,13 +26,12 @@ kotlin { } sourceSets { - val commonMain by getting - val commonTest by getting { + getByName("commonTest") { dependencies { implementation(kotlin("test")) } } - val jvmTest by getting { + getByName("jvmTest") { dependencies { implementation(libs.org.junit.jupiter) implementation(libs.com.google.truth) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt index 141e2db945ea..90f825d8397f 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt @@ -10,14 +10,14 @@ import app.aaps.core.keys.interfaces.Preferences */ abstract class PluginBaseWithPreferences( pluginDescription: PluginDescription, - val ownPreferences: List> = emptyList(), + val ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, rh: ResourceHelper, val preferences: Preferences ) : PluginBase(pluginDescription, aapsLogger, rh) { init { - ownPreferences.forEach { preferences.registerPreferences(it) } + preferences.registerPreferences(ownPreferences) } /** diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt index dd2e14bb7f2a..4a2be0f909a1 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.launch */ abstract class PumpPluginBase( pluginDescription: PluginDescription, - ownPreferences: List> = emptyList(), + ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, rh: ResourceHelper, preferences: Preferences, diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt index db04b3002892..7c92bc3a1a40 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt @@ -9,13 +9,6 @@ interface IntentPreferenceKey : PreferenceKey { val urlResId: Int? get() = null - /** - * Activity class to launch (for PreferenceType.ACTIVITY). - * If set, clicking the preference will launch this activity. - */ - val activityClass: Class<*>? - get() = null - /** * String resource ID for confirmation dialog message. * When set, clicking this preference shows an OK/Cancel dialog before executing onClick. @@ -31,13 +24,6 @@ interface IntentPreferenceKey : PreferenceKey { val onClick: (() -> Unit)? get() = null - /** - * Runtime-attached activity class. - * When set, this takes precedence over [activityClass]. - */ - val runtimeActivityClass: Class<*>? - get() = null - /** * Runtime-attached URL. * When set, this takes precedence over [urlResId]. @@ -65,14 +51,6 @@ class IntentKeyWithClick( override val onClick: () -> Unit ) : IntentPreferenceKey by delegate -/** - * Wrapper that attaches an activity class to an IntentPreferenceKey. - */ -class IntentKeyWithActivity( - private val delegate: IntentPreferenceKey, - override val runtimeActivityClass: Class<*> -) : IntentPreferenceKey by delegate - /** * Wrapper that attaches a URL to an IntentPreferenceKey. */ @@ -87,12 +65,6 @@ class IntentKeyWithUrl( fun IntentPreferenceKey.withClick(onClick: () -> Unit): IntentPreferenceKey = IntentKeyWithClick(this, onClick) -/** - * Creates a new IntentPreferenceKey with an activity class attached. - */ -fun IntentPreferenceKey.withActivity(activityClass: Class<*>): IntentPreferenceKey = - IntentKeyWithActivity(this, activityClass) - /** * Creates a new IntentPreferenceKey with a URL attached. */ diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt index 5cae08f6c736..a946a89fd0cd 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt @@ -597,10 +597,15 @@ interface Preferences { fun getDependingOn(key: String): List /** - * Make new class available to Preference system - * Called from PluginBase::init + * Make new keys available to the Preference system. + * Called from PluginBase::init, normally as `registerPreferences(MyKey.entries + MyOtherKey.entries)`. + * + * Takes the key constants rather than their enum class: a `Class` only exists on the JVM, and + * the only thing the old signature ever did with it was read `enumConstants`. + * + * Registering the same key twice is allowed and does nothing. */ - fun registerPreferences(clazz: Class) + fun registerPreferences(keys: List) /** * List all stored preferences formatters diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt index dfa125b377d3..1e1432cbf9c8 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt @@ -62,27 +62,27 @@ fun StringPreferenceKey.withEntries(entries: Map): StringPrefere StringKeyWithEntries(this, entries) /** - * Wrapper that attaches a context-dependent entries provider to a StringPreferenceKey. - * The provider is called at compose time with the current Context. + * Wrapper that attaches a runtime entries provider to a StringPreferenceKey. + * The provider is called at compose time, and the labels it returns are resolved there. * If the provider returns an empty map, shows a disabled preference with the empty message. */ class StringKeyWithEntriesProvider( private val delegate: StringPreferenceKey, - val entriesProvider: (android.content.Context) -> Map, - val emptyEntriesMessageResId: Int? = null + val entriesProvider: () -> Map, + val emptyEntriesMessage: TextRef? = null ) : StringPreferenceKey by delegate /** - * Creates a new StringPreferenceKey with a context-dependent entries provider. - * Use this when entries need to be resolved at compose time with Context access - * (e.g., Bluetooth devices requiring permission checks). + * Creates a new StringPreferenceKey with a runtime entries provider. + * Use this when the set of entries is only known at run time - for example when it depends on the + * connected pump model, or on devices found by a scan. * - * @param provider Function that takes Context and returns Map of stored value -> label - * @param emptyEntriesMessageResId Optional resource ID for message to show when entries are empty + * @param provider Function returning Map of stored value -> label + * @param emptyEntriesMessage Optional message to show when entries are empty * @return A new StringKeyWithEntriesProvider */ fun StringPreferenceKey.withEntriesProvider( - provider: (android.content.Context) -> Map, - emptyEntriesMessageResId: Int? = null + provider: () -> Map, + emptyEntriesMessage: TextRef? = null ): StringKeyWithEntriesProvider = - StringKeyWithEntriesProvider(this, provider, emptyEntriesMessageResId) \ No newline at end of file + StringKeyWithEntriesProvider(this, provider, emptyEntriesMessage) \ No newline at end of file diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index f5552668b264..6c67fbf9d587 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { mingwX64() sourceSets { - val commonMain by getting { + getByName("commonMain") { dependencies { api(libs.io.ktor.client.core) implementation(libs.io.ktor.client.content.negotiation) @@ -28,7 +28,7 @@ kotlin { api(libs.kotlinx.serialization.json) } } - val jvmMain by getting { + getByName("jvmMain") { dependencies { // OkHttp is both the Ktor engine and, on the JVM side, what the rest of the app // already uses. The client-control crypto in this source set is javax.crypto. @@ -36,14 +36,14 @@ kotlin { implementation(libs.io.ktor.client.okhttp) } } - val mingwX64Main by getting { + getByName("mingwX64Main") { dependencies { // CIO is Ktor's own multiplatform engine, enough for the compile proof. An Apple // target would use ktor-client-darwin instead. implementation(libs.io.ktor.client.cio) } } - val jvmTest by getting { + getByName("jvmTest") { dependencies { implementation(kotlin("test")) implementation(libs.org.junit.jupiter) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index 692b693d0503..0c31c9228f50 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -4,14 +4,12 @@ package app.aaps.core.ui.compose.preference -import android.content.Intent import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.IntentPreferenceKey @@ -108,41 +106,6 @@ fun AdaptiveUrlPreferenceItem( ) } -/** - * Composable dynamic activity preference for use inside card sections. - * - * @param title Optional title override. If null, uses intentKey.title - * @param summary Optional summary override. If null, uses intentKey.summary - */ -@Composable -fun AdaptiveDynamicActivityPreferenceItem( - intentKey: IntentPreferenceKey, - title: TextRef? = null, - activityClass: Class<*>, - summary: TextRef? = null, - visibilityContext: VisibilityContext? = null -) { - val effectiveTitle = title ?: intentKey.title - val effectiveSummary = summary ?: intentKey.summary - - val visibility = calculateIntentPreferenceVisibility( - intentKey = intentKey, - visibilityContext = visibilityContext - ) - - if (!visibility.visible) return - - val context = LocalContext.current - Preference( - title = { Text(stringResource(effectiveTitle)) }, - summary = effectiveSummary?.let { { Text(stringResource(it)) } }, - enabled = visibility.enabled, - onClick = if (visibility.enabled) { - { context.startActivity(Intent(context, activityClass)) } - } else null - ) -} - /** * Composable preference that navigates to an inline Compose screen. * Used for IntentPreferenceKey with composeScreen attached via withCompose(). diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index 5cd688e55298..fca4db6f4ff5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -8,7 +8,6 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.StringKey @@ -29,12 +28,11 @@ import app.aaps.core.ui.compose.stringResource * Automatically selects the appropriate composable. * * For LIST types, loads entries from resources using entriesResId/entryValuesResId. - * For URL/ACTIVITY types on IntentPreferenceKey, requires additional parameters. + * For URL types on IntentPreferenceKey, requires additional parameters. * * @param key The PreferenceKey to render * @param onIntentClick Optional click handler for IntentPreferenceKey with CLICK type * @param intentUrl Optional URL for IntentPreferenceKey with URL type - * @param intentActivityClass Optional Activity class for IntentPreferenceKey with ACTIVITY type */ @Composable fun AdaptivePreferenceItem( @@ -42,8 +40,7 @@ fun AdaptivePreferenceItem( onShowMessage: (String) -> Unit, visibilityContext: VisibilityContext? = null, onIntentClick: (() -> Unit)? = null, - intentUrl: String? = null, - intentActivityClass: Class<*>? = null + intentUrl: String? = null ) { when (key) { is BooleanPreferenceKey -> { @@ -106,10 +103,10 @@ fun AdaptivePreferenceItem( } is StringKeyWithEntriesProvider -> { - // Handle context-dependent entries provider - val context = LocalContext.current - val entries = remember(key) { key.entriesProvider(context) } - val emptyMessageResId = key.emptyEntriesMessageResId + // Entries are only known at run time, so the provider runs here and its labels are + // resolved in composable scope. + val entries = remember(key) { key.entriesProvider() }.mapValues { stringResource(it.value) } + val emptyMessage = key.emptyEntriesMessage if (entries.isNotEmpty()) { AdaptiveStringListPreferenceItem( @@ -117,11 +114,11 @@ fun AdaptivePreferenceItem( entries = entries, visibilityContext = visibilityContext ) - } else if (emptyMessageResId != null) { + } else if (emptyMessage != null) { // Show disabled preference with empty message Preference( title = { Text(stringResource(key.title)) }, - summary = { Text(stringResource(emptyMessageResId)) }, + summary = { Text(stringResource(emptyMessage)) }, enabled = false ) } @@ -193,11 +190,10 @@ fun AdaptivePreferenceItem( } is IntentPreferenceKey -> { - // Priority: 1) runtime click 2) compose screen 3) activity 4) url + // Priority: 1) runtime click 2) compose screen 3) url val resolvedClick = key.onClick ?: onIntentClick val resolvedCompose = key.composeScreen as? ComposeScreenContent val onNavigateToCompose = LocalNavigateToCompose.current - val resolvedActivity = key.runtimeActivityClass ?: intentActivityClass ?: key.activityClass val resolvedUrl = key.runtimeUrl ?: intentUrl ?: key.urlResId?.let { stringResource(it) } when { @@ -219,15 +215,6 @@ fun AdaptivePreferenceItem( ) } - resolvedActivity != null -> { - AdaptiveDynamicActivityPreferenceItem( - - intentKey = key, - activityClass = resolvedActivity, - visibilityContext = visibilityContext - ) - } - resolvedUrl != null -> { AdaptiveUrlPreferenceItem( diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt index 75180d635785..72ab83702af5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt @@ -129,7 +129,7 @@ private object PreviewPreferences : Preferences { override fun get(key: String): NonPreferenceKey? = null override fun getIfExists(key: String): NonPreferenceKey? = null override fun getDependingOn(key: String): List = emptyList() - override fun registerPreferences(clazz: Class) {} + override fun registerPreferences(keys: List) {} override fun allMatchingStrings(key: ComposedKey): List = emptyList() override fun allMatchingInts(key: ComposedKey): List = emptyList() override fun isExportableKey(key: String): Boolean = false diff --git a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt index 9cbca440f088..08c22e8d7a89 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt @@ -49,7 +49,7 @@ class LocalAlertUtilsImpl @Inject constructor( ) : LocalAlertUtils { init { - preferences.registerPreferences(LocalAlertLongKey::class.java) + preferences.registerPreferences(LocalAlertLongKey.entries) } private fun missedReadingsThreshold(): Long { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt index 06ad5db8cd66..c4ca02f075dc 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt @@ -75,25 +75,27 @@ class PreferencesImpl @Inject constructor( override val nsclientMode: Boolean = config.AAPSCLIENT override val pumpControlMode: Boolean = config.PUMPCONTROL - private val prefsList: MutableList> = - mutableListOf( - BooleanComposedKey::class.java, - BooleanKey::class.java, - BooleanNonKey::class.java, - DoubleKey::class.java, - IntentKey::class.java, - IntKey::class.java, - IntComposedKey::class.java, - IntNonKey::class.java, - LongComposedKey::class.java, - LongNonKey::class.java, - StringKey::class.java, - StringNonKey::class.java, - UnitDoubleKey::class.java, - ProfileComposedStringKey::class.java, - ProfileComposedBooleanKey::class.java, - ProfileIntKey::class.java, - ) + // A set, not a list. registerPreferences runs once per plugin at startup and used to hold enum + // classes, so it did about a dozen contains-checks in total. It now holds the key constants, and + // a list would mean a linear scan per key against well over a thousand of them. LinkedHashSet + // keeps insertion order, so everything that iterates this still sees the same sequence. + private val prefsList: MutableSet = + (BooleanComposedKey.entries + + BooleanKey.entries + + BooleanNonKey.entries + + DoubleKey.entries + + IntentKey.entries + + IntKey.entries + + IntComposedKey.entries + + IntNonKey.entries + + LongComposedKey.entries + + LongNonKey.entries + + StringKey.entries + + StringNonKey.entries + + UnitDoubleKey.entries + + ProfileComposedStringKey.entries + + ProfileComposedBooleanKey.entries + + ProfileIntKey.entries).toCollection(LinkedHashSet()) // Emits a key on every LOCAL write to a Bidirectional-synced preference (not on putRemote), so // the client→master sync publisher can push local edits. Buffered + DROP_OLDEST so a pref write @@ -102,7 +104,7 @@ class PreferencesImpl @Inject constructor( override val syncedLocalChanges: SharedFlow get() = _syncedLocalChanges override fun getSyncKeys(): List = - prefsList.flatMap { it.enumConstants!!.asIterable() }.filter { it.sync != null } + prefsList.filter { it.sync != null } /** Local edit of a synced key: bump its monotonic modified stamp and signal the publisher. */ private fun onLocalSyncedWrite(key: NonPreferenceKey) { @@ -378,24 +380,15 @@ class PreferencesImpl @Inject constructor( override fun get(key: String): NonPreferenceKey? = prefsList - .flatMap { it.enumConstants!!.asIterable() } .find { it.key == key } override fun getIfExists(key: String): NonPreferenceKey? = prefsList - .flatMap { it.enumConstants!!.asIterable() } .find { it.key == key } override fun getDependingOn(key: String): List = - mutableListOf().also { list -> - prefsList.forEach { clazz -> - if (PreferenceKey::class.java.isAssignableFrom(clazz)) - clazz.enumConstants!!.filter { - (it as PreferenceKey).dependency != null && it.dependency!!.key == key || it.negativeDependency != null && it.negativeDependency!!.key == key - }.forEach { - list.add(it as PreferenceKey) - } - } + prefsList.filterIsInstance().filter { + it.dependency?.key == key || it.negativeDependency?.key == key } override fun get(key: BooleanComposedNonPreferenceKey, vararg arguments: Any): Boolean = @@ -437,8 +430,8 @@ class PreferencesImpl @Inject constructor( return stringFlows.computeIfAbsent(composedKey) { MutableStateFlow(get(key, *arguments)) } } - override fun registerPreferences(clazz: Class) { - if (clazz !in prefsList) prefsList.add(clazz) + override fun registerPreferences(keys: List) { + prefsList.addAll(keys) } override fun allMatchingStrings(key: ComposedKey): List = @@ -459,7 +452,6 @@ class PreferencesImpl @Inject constructor( override fun isExportableKey(key: String): Boolean { prefsList - .flatMap { it.enumConstants!!.asIterable() } .forEach { if (it.key == key && it.exportable) return true if (it is ComposedKey && key.startsWith(it.key) && it.exportable) return true @@ -521,8 +513,5 @@ class PreferencesImpl @Inject constructor( } override fun getAllPreferenceKeys(): List = - prefsList - .filter { PreferenceKey::class.java.isAssignableFrom(it) } - .flatMap { it.enumConstants!!.asIterable() } - .filterIsInstance() + prefsList.filterIsInstance() } \ No newline at end of file diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index ad773196e0f8..b0d48341e5b0 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -100,7 +100,7 @@ class AutotunePlugin @Inject constructor( } .showInList { config.isEngineeringMode() && config.isDev() || config.isEnabled(ExternalOptions.ENABLE_AUTOTUNE) } .description(R.string.autotune_description), - ownPreferences = listOf(AutotuneStringKey::class.java), + ownPreferences = AutotuneStringKey.entries, aapsLogger, rh, preferences ), Autotune { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt index e2723e4ee2a5..228400b88ffe 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt @@ -94,7 +94,7 @@ class OpenAPSAMAPlugin @Inject constructor( .preferencesVisibleInSimpleMode(false) .showInList { config.APS || config.AAPSCLIENT } // AAPSCLIENT: visible so a client can select the master's APS .description(R.string.description_ama), - ownPreferences = listOf(ApsIntentKey::class.java), + ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences ), APS, PluginConstraints { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt index 7900fc9f1b81..cc262b3cec66 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt @@ -112,7 +112,7 @@ open class OpenAPSAutoISFPlugin @Inject constructor( .preferencesVisibleInSimpleMode(false) .showInList { (config.APS || config.AAPSCLIENT) && config.isEngineeringMode() && config.isDev() } // AAPSCLIENT: visible so a client can select the master's APS (still eng+dev only) .description(R.string.description_auto_isf), - ownPreferences = listOf(ApsIntentKey::class.java), + ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences ), APS, PluginConstraints { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index e1e15df36dbb..6df1fdfd44ed 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -115,7 +115,7 @@ open class OpenAPSSMBPlugin @Inject constructor( .showInList { config.APS || config.AAPSCLIENT } // AAPSCLIENT: visible so a client can select the master's APS .description(R.string.description_smb) .setDefault(), - ownPreferences = listOf(ApsIntentKey::class.java), + ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences ), APS, PluginConstraints { diff --git a/plugins/calibration/build.gradle.kts b/plugins/calibration/build.gradle.kts index de9e7f8b4123..34b0aef82b5b 100644 --- a/plugins/calibration/build.gradle.kts +++ b/plugins/calibration/build.gradle.kts @@ -19,7 +19,6 @@ android { dependencies { implementation(project(":core:data")) implementation(project(":core:interfaces")) - implementation(project(":core:keys")) implementation(project(":core:ui")) // Compose diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt index 8a1dfa718bba..14ddbfb4402b 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt @@ -40,7 +40,7 @@ class DstHelperPlugin @Inject constructor( .alwaysEnabled(true) .showInList { false } .pluginName(R.string.dst_plugin_name), - ownPreferences = listOf(DstHelperLongKey::class.java), + ownPreferences = DstHelperLongKey.entries, aapsLogger, rh, preferences ), DstHelper { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt index c573ae4f781c..42ddfbe69c09 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt @@ -44,7 +44,7 @@ class ObjectivesPlugin @Inject constructor( .shortName(R.string.objectives_shortname) .enableByDefault(config.APS) .description(R.string.description_objectives), - ownPreferences = listOf(ObjectivesBooleanComposedKey::class.java, ObjectivesLongComposedKey::class.java), + ownPreferences = ObjectivesBooleanComposedKey.entries + ObjectivesLongComposedKey.entries, aapsLogger, rh, preferences ), PluginConstraints, Objectives { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt index f3af08499825..8afabb50cc0c 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt @@ -51,7 +51,7 @@ class SignatureVerifierPlugin @Inject constructor( .alwaysEnabled(true) .showInList { false } .pluginName(R.string.signature_verifier), - ownPreferences = listOf(SignatureVerifierLongKey::class.java), + ownPreferences = SignatureVerifierLongKey.entries, aapsLogger, rh, preferences ), PluginConstraints { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt index 2aafc037c2ed..2b2dde0a397d 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt @@ -31,7 +31,7 @@ class VersionCheckerPlugin @Inject constructor( .alwaysEnabled(true) .showInList { false } .pluginName(R.string.version_checker), - ownPreferences = listOf(VersionCheckerLongKey::class.java), + ownPreferences = VersionCheckerLongKey.entries, aapsLogger, rh, preferences ), PluginConstraints { diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt index e12b3b171ea5..abb47c370bda 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt @@ -71,7 +71,7 @@ class UnscentedKalmanFilterPlugin @Inject constructor( .pluginName(R.string.UKF_name) .shortName(R.string.smoothing_shortname) .description(R.string.description_UKF), - ownPreferences = listOf(UkfLongNonKey::class.java, UkfIntNonKey::class.java, UkfDoubleNonKey::class.java), + ownPreferences = UkfLongNonKey.entries + UkfIntNonKey.entries + UkfDoubleNonKey.entries, aapsLogger, rh, preferences ), Smoothing { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt index 935832f0fb7e..2a6e716d8a55 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt @@ -13,7 +13,7 @@ import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef abstract class AbstractBgSourcePlugin( pluginDescription: PluginDescription, - ownPreferences: List> = emptyList(), + ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, rh: ResourceHelper, preferences: Preferences, diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt index e631cd908637..ed5129a6d2a0 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt @@ -12,7 +12,7 @@ import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef abstract class AbstractBgSourceWithSensorInsertLogPlugin( pluginDescription: PluginDescription, - ownPreferences: List> = emptyList(), + ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, rh: ResourceHelper, preferences: Preferences, diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt index 532333410b93..5f5dac758035 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt @@ -56,7 +56,7 @@ class GlunovoPlugin @Inject constructor( .shortName(R.string.glunovo) .preferencesVisibleInSimpleMode(false) .description(R.string.description_source_glunovo), - ownPreferences = listOf(GlunovoLongKey::class.java), + ownPreferences = GlunovoLongKey.entries, aapsLogger, resourceHelper, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt index fff8bbb4fec2..9aa2bad42b6f 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt @@ -56,7 +56,7 @@ class IntelligoPlugin @Inject constructor( .shortName(R.string.intelligo) .preferencesVisibleInSimpleMode(false) .description(R.string.description_source_intelligo), - ownPreferences = listOf(IntelligoLongKey::class.java), + ownPreferences = IntelligoLongKey.entries, aapsLogger, resourceHelper, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt index 4fd1d5a830a0..9e3240fb4d09 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt @@ -58,7 +58,7 @@ class InstaraPlugin @Inject constructor( .preferencesVisibleInSimpleMode(false) .description(app.aaps.plugins.source.R.string.description_source_instara_app), // Register Instara plugin-local preference/non-preference key enums - ownPreferences = listOf(InstaraBooleanKey::class.java, InstaraStringKey::class.java), + ownPreferences = InstaraBooleanKey.entries + InstaraStringKey.entries, aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index cc0f0b263242..51fc61dd12ff 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -64,7 +64,7 @@ class GarminPlugin @Inject constructor( .pluginName(R.string.garmin) .shortName(R.string.garmin) .description(R.string.garmin_description), - ownPreferences = listOf(GarminStringKey::class.java, GarminBooleanKey::class.java, GarminIntKey::class.java), + ownPreferences = GarminStringKey.entries + GarminBooleanKey.entries + GarminIntKey.entries, aapsLogger, resourceHelper, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 910d8c53b105..4f51bb1eb1e9 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -169,7 +169,7 @@ class NSClientV3Plugin @Inject constructor( title = rh.gs(R.string.ns_client_v3_title) ) }, - ownPreferences = listOf(NsclientBooleanKey::class.java, NsclientStringKey::class.java, NsclientLongKey::class.java), + ownPreferences = NsclientBooleanKey.entries + NsclientStringKey.entries + NsclientLongKey.entries, aapsLogger, rh, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt index a60fb0277564..b87f1f2bda66 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt @@ -87,7 +87,7 @@ class OpenHumansUploaderPlugin @Inject internal constructor( context = plugin.context, ) }, - ownPreferences = listOf(OhStringKey.AppId::class.java, OhLongKey.Counter::class.java), + ownPreferences = OhStringKey.entries + OhLongKey.entries, aapsLogger, rh, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index 51771d7a1063..4423535a3163 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -146,7 +146,7 @@ class SmsCommunicatorPlugin @Inject constructor( .pluginName(R.string.smscommunicator) .shortName(R.string.smscommunicator_shortname) .description(R.string.description_sms_communicator), - ownPreferences = listOf(SmsIntentKey::class.java), + ownPreferences = SmsIntentKey.entries, aapsLogger, rh, preferences ), SmsCommunicator { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index bbbf030dc655..a5dfb990e736 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -83,10 +83,7 @@ class TidepoolPlugin @Inject constructor( ) } .description(R.string.description_tidepool), - ownPreferences = listOf( - TidepoolBooleanKey::class.java, TidepoolLongNonKey::class.java, - TidepoolStringNonKey::class.java - ), + ownPreferences = TidepoolBooleanKey.entries + TidepoolLongNonKey.entries + TidepoolStringNonKey.entries, aapsLogger, rh, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 993148bc9421..b89ec48ade25 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -111,7 +111,7 @@ class XdripPlugin @Inject constructor( .pluginName(R.string.xdrip) .shortName(R.string.xdrip_shortname) .description(R.string.description_xdrip), - ownPreferences = listOf(XdripLongKey::class.java, XdripIntentKey::class.java), + ownPreferences = XdripLongKey.entries + XdripIntentKey.entries, aapsLogger, rh, preferences ) { diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index 22391ad54f7c..de929154a94c 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -137,10 +137,7 @@ class ComboV2Plugin @Inject constructor( .pluginName(R.string.combov2_plugin_name) .shortName(R.string.combov2_plugin_shortname) .description(R.string.combov2_plugin_description), - ownPreferences = listOf( - ComboIntKey::class.java, ComboBooleanKey::class.java, - ComboStringNonKey::class.java, ComboIntNonKey::class.java, ComboLongNonKey::class.java - ), + ownPreferences = ComboIntKey.entries + ComboBooleanKey.entries + ComboStringNonKey.entries + ComboIntNonKey.entries + ComboLongNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, PluginConstraints { diff --git a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt index 62d8f244c9a6..ac0b09081465 100644 --- a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt +++ b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt @@ -56,7 +56,7 @@ import javax.inject.Provider // When using this class, make sure that your first step is to create mConnection (see MedtronicPumpPlugin) abstract class PumpPluginAbstract protected constructor( pluginDescription: PluginDescription, - ownPreferences: List> = emptyList(), + ownPreferences: List = emptyList(), pumpType: PumpType, rh: ResourceHelper, aapsLogger: AAPSLogger, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt index 952ca95fc55f..f09527145863 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt @@ -89,7 +89,7 @@ abstract class AbstractDanaRPlugin protected constructor( .pluginName(app.aaps.pump.dana.R.string.danarpump) .shortName(app.aaps.pump.dana.R.string.danarpump_shortname) .description(app.aaps.pump.dana.R.string.description_pump_dana_r), - ownPreferences = listOf(DanaStringNonKey::class.java, DanaIntKey::class.java, DanaIntNonKey::class.java, DanaBooleanKey::class.java, DanaIntentKey::class.java), + ownPreferences = DanaStringNonKey.entries + DanaIntKey.entries + DanaIntNonKey.entries + DanaBooleanKey.entries + DanaIntentKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Dana, PumpPluginConstraints, OwnDatabasePlugin { diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt index 9f2d2b26f733..accf13831cbd 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt @@ -98,7 +98,8 @@ class DanaRSPlugin @Inject constructor( .pluginName(app.aaps.pump.dana.R.string.danarspump) .shortName(app.aaps.pump.dana.R.string.danarspump_shortname) .description(app.aaps.pump.dana.R.string.description_pump_dana_rs), - ownPreferences = listOf(DanaStringNonKey::class.java, DanaIntKey::class.java, DanaBooleanKey::class.java, DanaIntentKey::class.java, DanaStringComposedKey::class.java, DanaLongKey::class.java), + ownPreferences = DanaStringNonKey.entries + DanaIntKey.entries + DanaBooleanKey.entries + DanaIntentKey.entries + DanaStringComposedKey.entries + + DanaLongKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Dana, PumpPluginConstraints, OwnDatabasePlugin { diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt index 14282f518e58..d91ba581d12e 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt @@ -97,10 +97,8 @@ class DiaconnG8Plugin @Inject constructor( .pluginName(R.string.diaconn_g8_pump) .shortName(R.string.diaconn_g8_pump_shortname) .description(R.string.description_pump_diaconn_g8), - ownPreferences = listOf( - DiaconnIntentKey::class.java, DiaconnIntKey::class.java, DiaconnBooleanKey::class.java, - DiaconnStringNonKey::class.java, DiaconnIntNonKey::class.java, - ), + ownPreferences = DiaconnIntentKey.entries + DiaconnIntKey.entries + DiaconnBooleanKey.entries + DiaconnStringNonKey.entries + + DiaconnIntNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Diaconn, PumpPluginConstraints, OwnDatabasePlugin { diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index 018cc8f6a76f..823313352817 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -103,9 +103,7 @@ class EopatchPumpPlugin @Inject constructor( .pluginName(R.string.eopatch) .shortName(R.string.eopatch_shortname) .description(R.string.eopatch_pump_description), - ownPreferences = listOf( - EopatchIntKey::class.java, EopatchBooleanKey::class.java, EopatchStringNonKey::class.java - ), + ownPreferences = EopatchIntKey.entries + EopatchBooleanKey.entries + EopatchStringNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump { diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index 99ddbe914ae7..f1b4aa382243 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -107,10 +107,7 @@ class EquilPumpPlugin @Inject constructor( .pluginName(R.string.equil_name) .shortName(R.string.equil_name_short) .description(R.string.equil_pump_description), - ownPreferences = listOf( - EquilBooleanKey::class.java, EquilBooleanPreferenceKey::class.java, EquilIntPreferenceKey::class.java, - EquilStringKey::class.java - ), + ownPreferences = EquilBooleanKey.entries + EquilBooleanPreferenceKey.entries + EquilIntPreferenceKey.entries + EquilStringKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump { diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt index bf96386fa6c2..0bdd3484e1f1 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt @@ -177,10 +177,7 @@ class InsightPlugin @Inject constructor( appScope = appScope ) }, - ownPreferences = listOf( - InsightBooleanKey::class.java, InsightIntKey::class.java, - InsightLongNonKey::class.java, InsightDoubleNonKey::class.java, - ), + ownPreferences = InsightBooleanKey.entries + InsightIntKey.entries + InsightLongNonKey.entries + InsightDoubleNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Insight, PumpPluginConstraints, InsightConnectionService.StateCallback, OwnDatabasePlugin { diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index 7f01c52f5bb7..4c5eff8e4115 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -153,12 +153,9 @@ class MedtronicPumpPlugin @Inject constructor( .pluginName(R.string.medtronic_name) .shortName(R.string.medtronic_name_short) .description(R.string.description_pump_medtronic), - ownPreferences = listOf( - RileylinkBooleanPreferenceKey::class.java, RileyLinkDoubleKey::class.java, - RileyLinkLongKey::class.java, RileyLinkStringKey::class.java, RileyLinkStringPreferenceKey::class.java, - MedtronicBooleanPreferenceKey::class.java, MedtronicIntPreferenceKey::class.java, - MedtronicLongNonKey::class.java, MedtronicStringPreferenceKey::class.java - ), + ownPreferences = RileylinkBooleanPreferenceKey.entries + RileyLinkDoubleKey.entries + RileyLinkLongKey.entries + RileyLinkStringKey.entries + + RileyLinkStringPreferenceKey.entries + MedtronicBooleanPreferenceKey.entries + MedtronicIntPreferenceKey.entries + + MedtronicLongNonKey.entries + MedtronicStringPreferenceKey.entries, PumpType.MEDTRONIC_522_722, // we default to most basic model, correct model from config is loaded later rh = rh, aapsLogger = aapsLogger, diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index 44dfe87233b4..c9dede3f111c 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -39,6 +39,7 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withEntriesProvider import app.aaps.core.ui.compose.icons.IcPluginMedtrum import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -55,17 +56,17 @@ import app.aaps.pump.medtrum.keys.MedtrumStringNonKey import app.aaps.pump.medtrum.services.MedtrumService import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.math.abs +import kotlin.math.min import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.drop -import javax.inject.Inject -import javax.inject.Provider -import javax.inject.Singleton -import kotlin.math.abs -import kotlin.math.min @Singleton class MedtrumPlugin @Inject constructor( @@ -97,11 +98,8 @@ class MedtrumPlugin @Inject constructor( blePreCheck = blePreCheck ) }, - ownPreferences = listOf( - MedtrumStringKey::class.java, MedtrumIntKey::class.java, MedtrumBooleanKey::class.java, - MedtrumIntNonKey::class.java, MedtrumLongNonKey::class.java, MedtrumStringNonKey::class.java, MedtrumDoubleNonKey::class.java, - MedtrumBooleanNonKey::class.java - ), + ownPreferences = MedtrumStringKey.entries + MedtrumIntKey.entries + MedtrumBooleanKey.entries + MedtrumIntNonKey.entries + + MedtrumLongNonKey.entries + MedtrumStringNonKey.entries + MedtrumDoubleNonKey.entries + MedtrumBooleanNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Medtrum { @@ -416,7 +414,7 @@ class MedtrumPlugin @Inject constructor( titleResId = R.string.medtrum_pump_setting, items = listOf( MedtrumStringKey.MedtrumAlarmSettings.withEntriesProvider( - provider = { context -> getAlarmEntriesForPumpType(context) } + provider = { getAlarmEntriesForPumpType() } ), MedtrumBooleanKey.MedtrumWarningNotification, MedtrumBooleanKey.MedtrumPatchExpiration, @@ -458,23 +456,23 @@ class MedtrumPlugin @Inject constructor( preferences.put(MedtrumIntKey.MedtrumDailyMaxInsulin, min(preferences.get(MedtrumIntKey.MedtrumDailyMaxInsulin), MedtrumIntKey.MedtrumDailyMaxInsulin.max)) } - private fun getAlarmEntriesForPumpType(context: Context): Map { + private fun getAlarmEntriesForPumpType(): Map { // For NANO and 300U pumps, only Beep and Silent options are available return when (medtrumPump.pumpType()) { PumpType.MEDTRUM_NANO, PumpType.MEDTRUM_300U -> mapOf( - "6" to context.getString(R.string.alarm_setting_beep), - "7" to context.getString(R.string.alarm_setting_silent) + "6" to TextRef.Res(R.string.alarm_setting_beep), + "7" to TextRef.Res(R.string.alarm_setting_silent) ) else -> mapOf( - "0" to context.getString(R.string.alarm_setting_light_vibrate_beep), - "1" to context.getString(R.string.alarm_setting_light_vibrate), - "2" to context.getString(R.string.alarm_setting_light_beep), - "3" to context.getString(R.string.alarm_setting_light), - "4" to context.getString(R.string.alarm_setting_vibrate_beep), - "5" to context.getString(R.string.alarm_setting_vibrate), - "6" to context.getString(R.string.alarm_setting_beep), - "7" to context.getString(R.string.alarm_setting_silent) + "0" to TextRef.Res(R.string.alarm_setting_light_vibrate_beep), + "1" to TextRef.Res(R.string.alarm_setting_light_vibrate), + "2" to TextRef.Res(R.string.alarm_setting_light_beep), + "3" to TextRef.Res(R.string.alarm_setting_light), + "4" to TextRef.Res(R.string.alarm_setting_vibrate_beep), + "5" to TextRef.Res(R.string.alarm_setting_vibrate), + "6" to TextRef.Res(R.string.alarm_setting_beep), + "7" to TextRef.Res(R.string.alarm_setting_silent) ) } } diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index d1909905f93e..4113bd222453 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -134,10 +134,8 @@ class OmnipodDashPumpPlugin @Inject constructor( .pluginName(R.string.omnipod_dash_name) .shortName(R.string.omnipod_dash_name_short) .description(R.string.omnipod_dash_pump_description), - ownPreferences = listOf( - OmnipodBooleanPreferenceKey::class.java, OmnipodIntPreferenceKey::class.java, - DashBooleanPreferenceKey::class.java, DashStringNonPreferenceKey::class.java - ), + ownPreferences = OmnipodBooleanPreferenceKey.entries + OmnipodIntPreferenceKey.entries + DashBooleanPreferenceKey.entries + + DashStringNonPreferenceKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, OmnipodDash, OwnDatabasePlugin { diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index fc7218a7d5a3..5d5f0d75b7c6 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -162,9 +162,7 @@ class OmnipodErosPumpPlugin @Inject constructor( .pluginName(R.string.omnipod_eros_name) .shortName(R.string.omnipod_eros_name_short) .description(R.string.omnipod_eros_pump_description), - ownPreferences = listOf( - ErosBooleanPreferenceKey::class.java, ErosLongNonPreferenceKey::class.java, ErosStringNonPreferenceKey::class.java - ), + ownPreferences = ErosBooleanPreferenceKey.entries + ErosLongNonPreferenceKey.entries + ErosStringNonPreferenceKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, RileyLinkPumpDevice, OmnipodEros, OwnDatabasePlugin { diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt index 98a6f13a1450..76bd5923cee7 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt @@ -95,7 +95,7 @@ open class VirtualPumpPlugin @Inject constructor( .description(R.string.description_pump_virtual) .setDefault() .showInList { !config.AAPSCLIENT }, - ownPreferences = listOf(VirtualBooleanNonPreferenceKey::class.java), + ownPreferences = VirtualBooleanNonPreferenceKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, VirtualPump { diff --git a/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt b/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt index 07c260ff521b..75381937b9be 100644 --- a/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt +++ b/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt @@ -50,21 +50,19 @@ class PreferencesImpl @Inject constructor( override val nsclientMode: Boolean = false override val pumpControlMode: Boolean = false - private val prefsList: MutableList> = - mutableListOf( - BooleanKey::class.java, - BooleanNonKey::class.java, - IntKey::class.java, - IntNonKey::class.java, - IntComposedKey::class.java, - LongNonKey::class.java, - LongComposedKey::class.java, - DoubleKey::class.java, - UnitDoubleKey::class.java, - StringKey::class.java, - StringNonKey::class.java, - IntentKey::class.java, - ) + private val prefsList: MutableSet = + (BooleanKey.entries + + BooleanNonKey.entries + + IntKey.entries + + IntNonKey.entries + + IntComposedKey.entries + + LongNonKey.entries + + LongComposedKey.entries + + DoubleKey.entries + + UnitDoubleKey.entries + + StringKey.entries + + StringNonKey.entries + + IntentKey.entries).toCollection(LinkedHashSet()) private val booleanFlows = ConcurrentHashMap>() private val stringFlows = ConcurrentHashMap>() @@ -218,18 +216,15 @@ class PreferencesImpl @Inject constructor( override fun isUnitDependent(key: String): Boolean = prefsList - .flatMap { it.enumConstants!!.asIterable() } .filterIsInstance() .any { it.key == key } override fun get(key: String): NonPreferenceKey? = prefsList - .flatMap { it.enumConstants!!.asIterable() } .find { it.key == key } override fun getIfExists(key: String): NonPreferenceKey? = prefsList - .flatMap { it.enumConstants!!.asIterable() } .find { it.key == key } override fun get(key: BooleanComposedNonPreferenceKey, vararg arguments: Any): Boolean = @@ -262,19 +257,12 @@ class PreferencesImpl @Inject constructor( stringFlows.getOrPut(key.composeKey(*arguments)) { MutableStateFlow(get(key, *arguments)) } override fun getDependingOn(key: String): List = - mutableListOf().also { list -> - prefsList.forEach { clazz -> - if (PreferenceKey::class.java.isAssignableFrom(clazz)) - clazz.enumConstants!!.filter { - (it as PreferenceKey).dependency != null && it.dependency!!.key == key || it.negativeDependency != null && it.negativeDependency!!.key == key - }.forEach { - list.add(it as PreferenceKey) - } - } + prefsList.filterIsInstance().filter { + it.dependency?.key == key || it.negativeDependency?.key == key } - override fun registerPreferences(clazz: Class) { - if (clazz !in prefsList) prefsList.add(clazz) + override fun registerPreferences(keys: List) { + prefsList.addAll(keys) } override fun allMatchingStrings(key: ComposedKey): List = @@ -295,7 +283,6 @@ class PreferencesImpl @Inject constructor( override fun isExportableKey(key: String): Boolean { prefsList - .flatMap { it.enumConstants!!.asIterable() } .forEach { if (it.key == key) return true if (it is ComposedKey && key.startsWith(it.key)) return true @@ -304,8 +291,5 @@ class PreferencesImpl @Inject constructor( } override fun getAllPreferenceKeys(): List = - prefsList - .filter { PreferenceKey::class.java.isAssignableFrom(it) } - .flatMap { it.enumConstants!!.asIterable() } - .filterIsInstance() + prefsList.filterIsInstance() } \ No newline at end of file diff --git a/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt b/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt index 7a0d3521eafd..7a8e0c717d85 100644 --- a/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt @@ -133,10 +133,10 @@ internal class PreferencesImplTest { } @Test - fun registerPreferencesIsIdempotentForAlreadyRegisteredClass() { + fun registerPreferencesIsIdempotentForAlreadyRegisteredKeys() { val before = sut.getAllPreferenceKeys().size - // BooleanKey is already registered in the default prefsList -> adding again is a no-op - sut.registerPreferences(BooleanKey::class.java) + // BooleanKey is already in the default prefsList -> adding its keys again is a no-op + sut.registerPreferences(BooleanKey.entries) val after = sut.getAllPreferenceKeys().size assertThat(after).isEqualTo(before) } From 95da0fa7cabd47e3002fef149908b5e654a5f2aa Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 8 Aug 2026 08:43:08 +0200 Subject: [PATCH 014/146] more TextRef migration --- .../main/kotlin/app/aaps/core/keys/IntKey.kt | 11 +- .../kotlin/app/aaps/core/keys/StringKey.kt | 11 +- .../kotlin/app/aaps/core/keys/UnitType.kt | 65 +--- .../core/keys/interfaces/IntPreferenceKey.kt | 26 +- .../aaps/core/keys/interfaces/Preferences.kt | 7 - .../keys/interfaces/StringPreferenceKey.kt | 27 +- .../app/aaps/core/ui/compose/CarbTimeRow.kt | 8 +- .../app/aaps/core/ui/compose/FormatUtils.kt | 25 +- .../aaps/core/ui/compose/NumberInputRow.kt | 22 +- .../core/ui/compose/NumberInputRowPreviews.kt | 9 +- .../app/aaps/core/ui/compose/PlusMinusEdit.kt | 14 +- .../aaps/core/ui/compose/SliderWithButtons.kt | 29 +- .../ui/compose/SliderWithButtonsPreviews.kt | 5 +- .../app/aaps/core/ui/compose/UnitTypeText.kt | 85 ++++++ .../ui/compose/dialogs/ValueInputDialog.kt | 30 +- .../dialogs/ValueInputDialogPreviews.kt | 3 +- .../preference/AdaptiveDoublePreference.kt | 24 +- .../preference/AdaptiveIntPreference.kt | 24 +- .../preference/AdaptivePreferenceItem.kt | 22 +- .../AdaptiveUnitDoublePreference.kt | 3 +- .../preference/PreferenceSliderWithButtons.kt | 20 +- .../ui/compose/preference/PreviewUtils.kt | 1 - .../compose/dialogs/ValueInputDialogTest.kt | 3 +- .../sharedPreferences/PreferencesImpl.kt | 5 - .../compose/actions/ActionEditors.kt | 13 +- .../compose/triggers/TriggerEditors.kt | 35 +-- .../setupwizard/elements/SWRadioButton.kt | 4 +- .../constraints/safety/SafetyPlugin.kt | 3 +- .../dana/compose/DanaUserOptionsScreen.kt | 9 +- .../app/aaps/pump/dana/keys/DanaIntKey.kt | 5 +- .../aaps/pump/diaconn/keys/DiaconnIntKey.kt | 5 +- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 14 +- .../aaps/pump/eopatch/keys/EopatchIntKey.kt | 3 +- .../pump/equil/keys/EquilIntPreferenceKey.kt | 5 +- .../keys/MedtronicIntPreferenceKey.kt | 5 +- .../keys/MedtronicStringPreferenceKey.kt | 9 +- .../pump/medtrum/keys/MedtrumStringKey.kt | 5 +- .../common/keys/OmnipodIntPreferenceKey.kt | 3 +- .../keys/RileyLinkStringPreferenceKey.kt | 5 +- .../aaps/pump/virtual/VirtualPumpPlugin.kt | 3 +- .../CalibrationDialogScreen.kt | 5 +- .../compose/carbsDialog/CarbsDialogScreen.kt | 5 +- .../ui/compose/careDialog/CareDialogScreen.kt | 5 +- .../ExtendedBolusDialogScreen.kt | 5 +- .../ui/compose/fillDialog/FillDialogScreen.kt | 3 +- .../insulinDialog/InsulinDialogScreen.kt | 5 +- .../InsulinManagementScreen.kt | 7 +- .../profileHelper/ProfileHelperScreen.kt | 9 +- .../ProfileActivationScreen.kt | 7 +- .../quickLaunch/QuickLaunchConfigScreen.kt | 5 +- .../compose/quickWizard/QuickWizardEditor.kt | 289 +++++++++--------- .../aaps/ui/compose/scenes/ActionEditors.kt | 7 +- .../tempBasalDialog/TempBasalDialogScreen.kt | 7 +- .../ui/compose/tempTarget/TempTargetEditor.kt | 5 +- .../treatmentDialog/TreatmentDialogScreen.kt | 5 +- .../wizardDialog/WizardDialogScreen.kt | 9 +- .../wear/sharedPreferences/PreferencesImpl.kt | 5 - .../sharedPreferences/PreferencesImplTest.kt | 12 - 58 files changed, 499 insertions(+), 501 deletions(-) create mode 100644 core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt index 32d7bf30e03f..fa29e0f77fe1 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt @@ -17,7 +17,7 @@ enum class IntKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val calculatedDefaultValue: Boolean = false, override val showInApsMode: Boolean = true, @@ -256,7 +256,7 @@ enum class IntKey( titleResId = R.string.pref_title_protection_type_application, summaryResId = R.string.pref_summary_protection_type_application, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( ProtectionType.NONE.ordinal to R.string.noprotection, ProtectionType.BIOMETRIC.ordinal to R.string.biometric, ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, @@ -273,7 +273,7 @@ enum class IntKey( titleResId = R.string.pref_title_protection_type_bolus, summaryResId = R.string.pref_summary_protection_type_bolus, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( ProtectionType.NONE.ordinal to R.string.noprotection, ProtectionType.BIOMETRIC.ordinal to R.string.biometric, ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, @@ -293,7 +293,7 @@ enum class IntKey( titleResId = R.string.pref_title_protection_type_settings, summaryResId = R.string.pref_summary_protection_type_settings, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( ProtectionType.NONE.ordinal to R.string.noprotection, ProtectionType.BIOMETRIC.ordinal to R.string.biometric, ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, @@ -445,7 +445,7 @@ enum class IntKey( max = 2, titleResId = R.string.pref_title_site_rotation_profile, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( 0 to R.string.site_rotation_profile_man, 1 to R.string.site_rotation_profile_woman, 2 to R.string.site_rotation_profile_child @@ -455,5 +455,6 @@ enum class IntKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt index 66f0de088a7c..37485de1a74d 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt @@ -16,7 +16,7 @@ enum class StringKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -39,7 +39,7 @@ enum class StringKey( defaultValue = "mg/dl", titleResId = R.string.pref_title_units, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "mg/dl" to R.string.units_mgdl, "mmol" to R.string.units_mmol ), @@ -50,7 +50,7 @@ enum class StringKey( defaultValue = "default", titleResId = R.string.pref_title_language, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "default" to R.string.lang_default, "en" to R.string.lang_en, "af" to R.string.lang_af, @@ -93,7 +93,7 @@ enum class StringKey( titleResId = R.string.pref_title_app_color_scheme, summaryResId = R.string.pref_summary_theme_switcher, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "dark" to R.string.pref_dark_theme, "light" to R.string.pref_light_theme, "system" to R.string.pref_follow_system_theme @@ -143,7 +143,7 @@ enum class StringKey( defaultValue = "PASSIVE", titleResId = R.string.pref_title_automation_location, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "PASSIVE" to R.string.automation_location_passive, "NETWORK" to R.string.automation_location_network, "GPS" to R.string.automation_location_gps @@ -204,5 +204,6 @@ enum class StringKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt index 2473526e7d1a..67a4428a058f 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt @@ -3,6 +3,9 @@ package app.aaps.core.keys /** * Enum defining unit types for preference values. * Used to format values with appropriate units in UI. + * + * The mapping from a unit to its text lives in `:core:ui` (`UnitTypeText.kt`), not here, so that + * this file stays free of Android resources. */ enum class UnitType { @@ -23,50 +26,6 @@ enum class UnitType { MGDL } -/** - * Returns the resource ID for formatting a single value with this unit type. - * Use with stringResource(resId, value) in Compose. - */ -fun UnitType.valueResId(): Int? = when (this) { - UnitType.NONE -> null - UnitType.GRAMS -> R.string.units_format_grams - UnitType.MIN -> R.string.units_format_min - UnitType.SEC -> R.string.units_format_sec - UnitType.HOURS -> R.string.units_format_hours - UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double - UnitType.DAYS -> R.string.units_format_days - UnitType.PERCENT -> R.string.units_format_percent - UnitType.INSULIN -> R.string.units_format_insulin - UnitType.INSULIN_INT -> R.string.units_format_insulin_int - UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate - UnitType.DOUBLE -> R.string.units_format_double - UnitType.DOUBLE_2 -> R.string.units_format_double_2 - UnitType.DOUBLE_3 -> R.string.units_format_double_3 - UnitType.MGDL -> R.string.units_format_mgdl -} - -/** - * Returns the resource ID for formatting a value with range (value, min, max). - * Use with stringResource(resId, value, min, max) in Compose. - */ -fun UnitType.rangeResId(): Int? = when (this) { - UnitType.NONE -> null - UnitType.GRAMS -> R.string.units_format_grams_range - UnitType.MIN -> R.string.units_format_min_range - UnitType.SEC -> R.string.units_format_sec_range - UnitType.HOURS -> R.string.units_format_hours_range - UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double_range - UnitType.DAYS -> R.string.units_format_days_range - UnitType.PERCENT -> R.string.units_format_percent_range - UnitType.INSULIN -> R.string.units_format_insulin_range - UnitType.INSULIN_INT -> R.string.units_format_insulin_int_range - UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate_range - UnitType.DOUBLE -> R.string.units_format_double_range - UnitType.DOUBLE_2 -> R.string.units_format_double_2_range - UnitType.DOUBLE_3 -> R.string.units_format_double_3_range - UnitType.MGDL -> R.string.units_format_mgdl_range -} - /** * Returns the number of decimal places for this unit type. */ @@ -86,21 +45,3 @@ fun UnitType.step(): Double = when (this) { UnitType.INSULIN, UnitType.INSULIN_RATE, UnitType.DOUBLE, UnitType.HOURS_DOUBLE -> 0.1 else -> 1.0 } - -/** - * Returns the resource ID for the unit label string (e.g., "min", "U", "h"). - * Use with stringResource(resId) in Compose for slider value display. - */ -fun UnitType.unitLabelResId(): Int? = when (this) { - UnitType.NONE -> null - UnitType.GRAMS -> R.string.units_grams - UnitType.MIN -> R.string.units_min - UnitType.SEC -> R.string.units_sec - UnitType.HOURS, UnitType.HOURS_DOUBLE -> R.string.units_hours - UnitType.DAYS -> R.string.units_days - UnitType.PERCENT -> R.string.units_percent - UnitType.INSULIN, UnitType.INSULIN_INT -> R.string.units_insulin - UnitType.INSULIN_RATE -> R.string.units_insulin_rate - UnitType.DOUBLE, UnitType.DOUBLE_2, UnitType.DOUBLE_3 -> null // No unit label for generic doubles - UnitType.MGDL -> R.string.units_mgdl -} diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt index f5ad7da82267..796c7bfff0a5 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt @@ -29,36 +29,32 @@ interface IntPreferenceKey : PreferenceKey, IntNonPreferenceKey { /** * Entries for LIST type preferences. - * Map of stored value -> label resource ID. + * Map of stored value -> label. * Empty map means no entries (not a list preference). */ - val entries: Map + val entries: Map get() = emptyMap() - /** - * Runtime-resolved entries for LIST type preferences. - * Map of stored value -> resolved label string. - * When set, this takes precedence over [entries] resource IDs. - */ - val resolvedEntries: Map? - get() = null } /** - * Wrapper that attaches runtime-resolved entries to an IntPreferenceKey. + * Wrapper that attaches entries to an IntPreferenceKey. * Uses delegation to preserve all other properties from the original key. */ class IntKeyWithEntries( private val delegate: IntPreferenceKey, - override val resolvedEntries: Map + override val entries: Map ) : IntPreferenceKey by delegate /** - * Creates a new IntPreferenceKey with runtime-resolved entries attached. - * Use this when entries need to be resolved at runtime (e.g., programmatic values). + * Creates a new IntPreferenceKey with entries attached. + * Use this when the entries are only known at run time - a generated range, or a list that depends + * on the connected device. * - * @param entries Map of stored value -> resolved label string + * @param entries Map of stored value -> label. Use [TextRef.Res] with arguments for anything the + * user reads, so it stays translatable; [TextRef.Literal] only for text that is genuinely not a + * resource, such as a device name. * @return A new IntPreferenceKey with the entries attached */ -fun IntPreferenceKey.withEntries(entries: Map): IntPreferenceKey = +fun IntPreferenceKey.withEntries(entries: Map): IntPreferenceKey = IntKeyWithEntries(this, entries) \ No newline at end of file diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt index a946a89fd0cd..bcdbd9abc83e 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt @@ -589,13 +589,6 @@ interface Preferences { */ fun getIfExists(key: String): NonPreferenceKey? - /** - * Find all [app.aaps.core.keys.interfaces.PreferenceKey] which have `dependency` or `negativeDependency` - * @param key string representation of key - * @return list of [app.aaps.core.keys.interfaces.PreferenceKey] - */ - fun getDependingOn(key: String): List - /** * Make new keys available to the Preference system. * Called from PluginBase::init, normally as `registerPreferences(MyKey.entries + MyOtherKey.entries)`. diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt index 1e1432cbf9c8..6829f6132110 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt @@ -19,20 +19,12 @@ interface StringPreferenceKey : PreferenceKey, StringNonPreferenceKey { /** * Entries for LIST type preferences. - * Map of stored value -> label resource ID. + * Map of stored value -> label. * Empty map means no entries (not a list preference). */ - val entries: Map + val entries: Map get() = emptyMap() - /** - * Runtime-resolved entries for LIST type preferences. - * Map of stored value -> resolved label string. - * When set, this takes precedence over [entries] resource IDs. - */ - val resolvedEntries: Map? - get() = null - /** * Validator for the string value. * Used to validate input before accepting it. @@ -43,22 +35,25 @@ interface StringPreferenceKey : PreferenceKey, StringNonPreferenceKey { } /** - * Wrapper that attaches runtime-resolved entries to a StringPreferenceKey. + * Wrapper that attaches entries to a StringPreferenceKey. * Uses delegation to preserve all other properties from the original key. */ class StringKeyWithEntries( private val delegate: StringPreferenceKey, - override val resolvedEntries: Map + override val entries: Map ) : StringPreferenceKey by delegate /** - * Creates a new StringPreferenceKey with runtime-resolved entries attached. - * Use this when entries need to be resolved at runtime (e.g., from plugins). + * Creates a new StringPreferenceKey with entries attached. + * Use this when the entries are only known at run time - for example a list that depends on the + * connected device, or on values computed from another setting. * - * @param entries Map of stored value -> resolved label string + * @param entries Map of stored value -> label. Use [TextRef.Res] with arguments for anything the + * user reads, so it stays translatable; [TextRef.Literal] only for text that is genuinely not a + * resource, such as a device name. * @return A new StringPreferenceKey with the entries attached */ -fun StringPreferenceKey.withEntries(entries: Map): StringPreferenceKey = +fun StringPreferenceKey.withEntries(entries: Map): StringPreferenceKey = StringKeyWithEntries(this, entries) /** diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt index 10ce7e0b2a02..c5e075c2659c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Notifications -import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -29,6 +28,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.keys.R as KeysR @@ -68,7 +68,9 @@ fun CarbTimeRow( var expanded by rememberSaveable { mutableStateOf(false) } val expandRequester = rememberBringIntoViewOnExpand(expanded) - Column(modifier = modifier.fillMaxWidth().bringIntoViewRequester(expandRequester)) { + Column(modifier = modifier + .fillMaxWidth() + .bringIntoViewRequester(expandRequester)) { // Header row: icon + label + value + Change/OK button Row( verticalAlignment = Alignment.CenterVertically, @@ -140,7 +142,7 @@ fun CarbTimeRow( onValueChange = { onOffsetChange(it.toInt()) }, valueRange = offsetRange.first.toDouble()..offsetRange.last.toDouble(), step = offsetStep.toDouble(), - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) // Alarm toggle (disabled when offset <= 0) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt index 83ade0f7f6ea..4e4177ad0448 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import kotlin.math.abs import kotlin.math.roundToInt -import app.aaps.core.keys.R as KeysR /** * Formats minutes as duration string: "X h Y min" when >= 60 (omits minutes if zero), "X min" otherwise. @@ -45,32 +45,31 @@ fun formatMinutesAsDuration(minutes: Int, rh: ResourceHelper): String { } /** - * Formats a slider/input value for display, handling minutes-as-duration, resource format strings, + * Formats a slider/input value for display, handling durations, resource format strings, * unit labels, and plain value formatting. * * Priority order: - * 1. Minutes unit (unitLabelResId == units_min) → "X h Y min" or "X min" + * 1. [asDuration] → "X h Y min" or "X min" * 2. valueFormatResId → stringResource with value (as Int if formatAsInt, else Double) - * 3. unitLabel non-empty → "formatted_value unitLabel" + * 3. unitLabel set → "formatted_value unitLabel" * 4. Plain → valueFormat.format(value) + * + * @param asDuration render the value as a duration. This used to be inferred by comparing the label + * against `units_min`, which tied the behaviour to one particular string resource. Callers say what + * they mean instead. */ @Composable fun formatSliderDisplayValue( value: Double, - unitLabelResId: Int = 0, + unitLabel: TextRef? = null, valueFormatResId: Int? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat, - unitLabel: String = "" + asDuration: Boolean = false ): String { - val isMinutesUnit = unitLabelResId == KeysR.string.units_min - val resolvedUnitLabel = when { - unitLabelResId != 0 -> stringResource(unitLabelResId) - unitLabel.isNotEmpty() -> unitLabel - else -> "" - } + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" return when { - isMinutesUnit -> formatMinutesAsDuration(value.roundToInt()) + asDuration -> formatMinutesAsDuration(value.roundToInt()) valueFormatResId != null -> { if (formatAsInt) stringResource(valueFormatResId, value.roundToInt()) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index 92fe7c3357c7..bf611ddbcbd3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import kotlin.math.roundToInt @@ -52,8 +53,8 @@ import kotlin.math.roundToInt * @param onValueChange Callback invoked when value changes, receives new value as Double * @param valueRange The range of values the input can represent * @param step Step increment for +/- buttons - * @param unitLabelResId Resource ID for unit label (e.g., R.string.units_min, R.string.units_percent) - * @param unitLabel Resolved unit label string (used when unitLabelResId is 0) + * @param unitLabel Unit label shown after the value + * @param asDuration Render the value as "Xh Ym" instead of a plain number * @param valueFormatResId Resource ID for formatting value with unit (e.g., "%1$.1f U") * @param formatAsInt If true, value is formatted as Int for stringResource (use with %d format strings) * @param valueFormat Custom NumberFormat (overrides auto-created from decimalPlaces) @@ -75,8 +76,8 @@ fun NumberInputRow( valueRange: ClosedFloatingPointRange, step: Double, modifier: Modifier = Modifier, - unitLabelResId: Int = 0, - unitLabel: String = "", + unitLabel: TextRef? = null, + asDuration: Boolean = false, valueFormatResId: Int? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat? = null, @@ -111,19 +112,21 @@ fun NumberInputRow( } } + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" + // Formatted display text for special cases (duration) val formattedDisplay = formatSliderDisplayValue( value = value, - unitLabelResId = unitLabelResId, + unitLabel = unitLabel, valueFormatResId = valueFormatResId, formatAsInt = formatAsInt, valueFormat = effectiveValueFormat, - unitLabel = unitLabel + asDuration = asDuration ) // Only show formatted display when it differs meaningfully from the raw number val rawDisplay = effectiveValueFormat.format(value) val showFormattedDisplay = formattedDisplay != rawDisplay && - formattedDisplay != "$rawDisplay $unitLabel".trim() + formattedDisplay != "$rawDisplay $resolvedUnitLabel".trim() // Range text val rangeText = "${effectiveValueFormat.format(valueRange.start)} — ${effectiveValueFormat.format(valueRange.endInclusive)}" @@ -173,11 +176,6 @@ fun NumberInputRow( onValueChange(newValue) } - val resolvedUnitLabel = when { - unitLabelResId != 0 -> stringResource(unitLabelResId) - unitLabel.isNotEmpty() -> unitLabel - else -> "" - } Row( verticalAlignment = Alignment.CenterVertically, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt index adfe14381c65..0d3b5c0e7132 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt @@ -3,6 +3,7 @@ package app.aaps.core.ui.compose import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.keys.R as KeysR @@ -25,7 +26,7 @@ internal fun NumberInputRowWithUnitPreview() { valueRange = 0.0..10.0, step = 0.1, decimalPlaces = 1, - unitLabel = "U" + unitLabel = TextRef.Literal("U") ) } } @@ -40,7 +41,7 @@ internal fun NumberInputRowMinutesPreview() { onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) } } @@ -55,7 +56,7 @@ internal fun NumberInputRowPercentPreview() { onValueChange = {}, valueRange = 10.0..200.0, step = 5.0, - unitLabelResId = KeysR.string.units_percent + unitLabel = TextRef.Res(KeysR.string.units_percent) ) } } @@ -70,7 +71,7 @@ internal fun NumberInputRowMinutesDirectPreview() { onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt index 82d5d6a8f9d5..b5d8898917fb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt @@ -23,13 +23,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef import kotlin.math.roundToInt /** @@ -45,8 +45,7 @@ import kotlin.math.roundToInt * @param valueRange Allowed value range; values outside are clamped on commit * @param step Step size for +/- buttons * @param valueFormat Format for the displayed text - * @param unitLabel Resolved unit label (used when [unitLabelResId] is 0) - * @param unitLabelResId Resource ID for the unit label, shown as the field's trailing icon + * @param unitLabel Unit label, shown as the field's trailing icon * @param enabled Whether the stepper is interactive * @param modifier Modifier for the row container */ @@ -57,8 +56,7 @@ fun PlusMinusEdit( valueRange: ClosedFloatingPointRange, step: Double, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, - unitLabel: String = "", - unitLabelResId: Int = 0, + unitLabel: TextRef? = null, enabled: Boolean = true, modifier: Modifier = Modifier ) { @@ -80,11 +78,7 @@ fun PlusMinusEdit( } } - val resolvedUnitLabel = when { - unitLabelResId != 0 -> stringResource(unitLabelResId) - unitLabel.isNotEmpty() -> unitLabel - else -> "" - } + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" fun validateAndCommit(text: String) { val cleaned = text.trim().replace(",", ".") diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt index b566ccf71690..b590628aeb0f 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt @@ -30,15 +30,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.dialogs.ValueInputDialog import kotlinx.coroutines.delay import kotlin.math.roundToInt -import app.aaps.core.keys.R as KeysR /** * A Slider with +/- buttons on each side for fine-grained value control. @@ -53,8 +52,8 @@ import app.aaps.core.keys.R as KeysR * @param valueFormatResId Resource ID for formatting value with unit (e.g., "%1$.1f U" or "%1$d min") * @param formatAsInt If true, value is formatted as Int for stringResource (use with %d format strings) * @param valueFormat Format for the value (used for dialog and fallback) - * @param unitLabel Unit label for dialog input suffix (deprecated, use unitLabelResId) - * @param unitLabelResId Resource ID for unit label. When R.string.units_min, auto-formats as "Xh Ym" + * @param unitLabel Unit label, shown after the value and as the dialog input suffix + * @param asDuration Render the value as "Xh Ym" instead of a plain number * @param dialogLabel Label for the input dialog * @param dialogSummary Summary/description for the input dialog * @param modifier Modifier for the Row container @@ -75,8 +74,8 @@ fun SliderWithButtons( valueFormatResId: Int? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, - unitLabel: String = "", - unitLabelResId: Int = 0, + unitLabel: TextRef? = null, + asDuration: Boolean = false, dialogLabel: String? = null, dialogSummary: String? = null, enabled: Boolean = true, @@ -157,22 +156,16 @@ fun SliderWithButtons( val dynamicStepPosUp = (posForCurrentPlusStep - posForCurrent).coerceAtLeast(0.001f) val dynamicStepPosDown = (posForCurrent - posForCurrentMinusStep).coerceAtLeast(0.001f) - // Check if this is minutes input for special formatting - val isMinutesUnit = unitLabelResId == KeysR.string.units_min - val resolvedUnitLabel = when { - unitLabelResId != 0 -> stringResource(unitLabelResId) - unitLabel.isNotEmpty() -> unitLabel - else -> "" - } + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" // Use shared formatting function for display text val displayText = if (showValue) formatSliderDisplayValue( value = value, - unitLabelResId = unitLabelResId, + unitLabel = unitLabel, valueFormatResId = valueFormatResId, formatAsInt = formatAsInt, valueFormat = valueFormat, - unitLabel = unitLabel + asDuration = asDuration ) else "" BoxWithConstraints(modifier = modifier) { @@ -243,7 +236,7 @@ fun SliderWithButtons( color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.End, modifier = Modifier - .widthIn(min = if (isMinutesUnit || valueFormatResId != null || resolvedUnitLabel.isNotEmpty()) 70.dp else 40.dp) + .widthIn(min = if (asDuration || valueFormatResId != null || resolvedUnitLabel.isNotEmpty()) 70.dp else 40.dp) .then(if (enabled) Modifier.clickable { showDialog = true } else Modifier) .padding(start = 4.dp) ) @@ -259,8 +252,8 @@ fun SliderWithButtons( step = step, label = dialogLabel, summary = dialogSummary, - unitLabel = resolvedUnitLabel, - unitLabelResId = unitLabelResId, + unitLabel = unitLabel, + asDuration = asDuration, valueFormat = valueFormat, onValueConfirm = onValueChange, onDismiss = { showDialog = false } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt index 963505fa1948..a4a755956d8b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.R as KeysR @Preview(showBackground = true) @@ -31,7 +32,7 @@ internal fun SliderWithButtonsValuePreview() { step = 0.1, showValue = true, valueFormat = NumberFormat.DECIMAL_1, - unitLabel = "U" + unitLabel = TextRef.Literal("U") ) } } @@ -47,7 +48,7 @@ internal fun SliderWithButtonsIntPreview() { step = 5.0, showValue = true, valueFormat = NumberFormat.INTEGER, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt new file mode 100644 index 000000000000..00dab17087fe --- /dev/null +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt @@ -0,0 +1,85 @@ +package app.aaps.core.ui.compose + +import app.aaps.core.keys.UnitType +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.R as KeysR + +/** + * Maps a [UnitType] to the text that describes it. + * + * This lives in `:core:ui` rather than next to the enum on purpose. [UnitType] itself is a plain + * enum with no Android in it, so it can move to a multiplatform module. The mapping to string + * resources cannot, so it stays on the UI side and the enum travels alone. + */ + +/** + * The unit label on its own - "min", "U", "h". + * Null when the unit has no label, which is the case for NONE and the generic doubles. + */ +fun UnitType.unitLabel(): TextRef? = when (this) { + UnitType.NONE -> null + UnitType.GRAMS -> TextRef.Res(KeysR.string.units_grams) + UnitType.MIN -> TextRef.Res(KeysR.string.units_min) + UnitType.SEC -> TextRef.Res(KeysR.string.units_sec) + UnitType.HOURS, UnitType.HOURS_DOUBLE -> TextRef.Res(KeysR.string.units_hours) + UnitType.DAYS -> TextRef.Res(KeysR.string.units_days) + UnitType.PERCENT -> TextRef.Res(KeysR.string.units_percent) + UnitType.INSULIN, UnitType.INSULIN_INT -> TextRef.Res(KeysR.string.units_insulin) + UnitType.INSULIN_RATE -> TextRef.Res(KeysR.string.units_insulin_rate) + UnitType.DOUBLE, UnitType.DOUBLE_2, UnitType.DOUBLE_3 -> null // generic doubles carry no unit + UnitType.MGDL -> TextRef.Res(KeysR.string.units_mgdl) +} + +/** + * A value and its range, already filled in - "5 min (0 - 30)". + * + * The arguments are taken here rather than returned as a bare template id, because a [TextRef.Res] + * with no arguments renders the raw `%1$d` template. Taking them makes that mistake impossible. + */ +fun UnitType.rangeText(value: Any, min: Any, max: Any): TextRef? = + rangeFormatResId()?.let { TextRef.Res(it, listOf(value, min, max)) } + +/** + * Format template for a single value, e.g. `%1$d min`. + * + * Still a raw resource id, unlike the two above, because the slider applies it to whatever value + * the user drags to - the arguments are not known here. It never leaves `:core:ui`. + */ +fun UnitType.valueFormatResId(): Int? = when (this) { + UnitType.NONE -> null + UnitType.GRAMS -> KeysR.string.units_format_grams + UnitType.MIN -> KeysR.string.units_format_min + UnitType.SEC -> KeysR.string.units_format_sec + UnitType.HOURS -> KeysR.string.units_format_hours + UnitType.HOURS_DOUBLE -> KeysR.string.units_format_hours_double + UnitType.DAYS -> KeysR.string.units_format_days + UnitType.PERCENT -> KeysR.string.units_format_percent + UnitType.INSULIN -> KeysR.string.units_format_insulin + UnitType.INSULIN_INT -> KeysR.string.units_format_insulin_int + UnitType.INSULIN_RATE -> KeysR.string.units_format_insulin_rate + UnitType.DOUBLE -> KeysR.string.units_format_double + UnitType.DOUBLE_2 -> KeysR.string.units_format_double_2 + UnitType.DOUBLE_3 -> KeysR.string.units_format_double_3 + UnitType.MGDL -> KeysR.string.units_format_mgdl +} + +private fun UnitType.rangeFormatResId(): Int? = when (this) { + UnitType.NONE -> null + UnitType.GRAMS -> KeysR.string.units_format_grams_range + UnitType.MIN -> KeysR.string.units_format_min_range + UnitType.SEC -> KeysR.string.units_format_sec_range + UnitType.HOURS -> KeysR.string.units_format_hours_range + UnitType.HOURS_DOUBLE -> KeysR.string.units_format_hours_double_range + UnitType.DAYS -> KeysR.string.units_format_days_range + UnitType.PERCENT -> KeysR.string.units_format_percent_range + UnitType.INSULIN -> KeysR.string.units_format_insulin_range + UnitType.INSULIN_INT -> KeysR.string.units_format_insulin_int_range + UnitType.INSULIN_RATE -> KeysR.string.units_format_insulin_rate_range + UnitType.DOUBLE -> KeysR.string.units_format_double_range + UnitType.DOUBLE_2 -> KeysR.string.units_format_double_2_range + UnitType.DOUBLE_3 -> KeysR.string.units_format_double_3_range + UnitType.MGDL -> KeysR.string.units_format_mgdl_range +} + +/** True when this unit should be rendered as a duration ("1 h 30 min") rather than a plain number. */ +fun UnitType.isDuration(): Boolean = this == UnitType.MIN diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt index ce8a10947091..b772f22b20e9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt @@ -25,12 +25,12 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R - import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.R import app.aaps.core.ui.compose.formatMinutesAsDuration +import app.aaps.core.ui.compose.stringResource import kotlin.math.roundToInt -import app.aaps.core.keys.R as KeysR /** * Dialog for entering a numeric value directly. @@ -41,7 +41,7 @@ import app.aaps.core.keys.R as KeysR * @param label Optional label for the input field * @param summary Optional summary/description text to show below the label * @param unitLabel Optional unit label to show after value - * @param unitLabelResId Resource ID for unit label. When R.string.units_min, shows formatted preview + * @param asDuration Show a "= Xh Ym" preview under the field * @param valueFormat Format for displaying/parsing the value * @param onValueConfirm Called when user confirms with a valid value * @param onDismiss Called when dialog is dismissed @@ -55,8 +55,8 @@ fun ValueInputDialog( step: Double = 0.1, label: String? = null, summary: String? = null, - unitLabel: String = "", - unitLabelResId: Int = 0, + unitLabel: TextRef? = null, + asDuration: Boolean = false, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, onValueConfirm: (Double) -> Unit, onDismiss: () -> Unit @@ -68,34 +68,32 @@ fun ValueInputDialog( } var isError by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf("") } - - // Check if this is minutes input for formatted preview - val isMinutesUnit = unitLabelResId == KeysR.string.units_min + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" fun validateAndParse(): Double? { val text = textFieldValue.text.replace(",", ".") return try { val parsed = text.toDouble() when { - parsed < valueRange.start -> { + parsed < valueRange.start -> { isError = true errorMessage = "Min: ${valueFormat.format(valueRange.start)}" null } - parsed > valueRange.endInclusive -> { + parsed > valueRange.endInclusive -> { isError = true errorMessage = "Max: ${valueFormat.format(valueRange.endInclusive)}" null } - isMinutesUnit && parsed != parsed.roundToInt().toDouble() -> { + asDuration && parsed != parsed.roundToInt().toDouble() -> { isError = true errorMessage = "Minutes must be whole numbers" null } - else -> { + else -> { isError = false // Accept value as-is (no rounding to step) parsed @@ -116,7 +114,7 @@ fun ValueInputDialog( } // Compute formatted preview for minutes - val formattedPreview: String? = if (isMinutesUnit) { + val formattedPreview: String? = if (asDuration) { val minutes = textFieldValue.text.replace(",", ".").toDoubleOrNull()?.roundToInt() if (minutes != null && minutes >= 60) { "= ${formatMinutesAsDuration(minutes)}" @@ -162,8 +160,8 @@ fun ValueInputDialog( else -> null }, - suffix = if (unitLabel.isNotEmpty()) { - { Text(unitLabel) } + suffix = if (resolvedUnitLabel.isNotEmpty()) { + { Text(resolvedUnitLabel) } } else null, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Decimal, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt index 7d16eb37fc0b..f81a33a7fcf2 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt @@ -3,6 +3,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import app.aaps.core.keys.interfaces.TextRef @Preview(showBackground = true) @Composable @@ -13,7 +14,7 @@ internal fun ValueInputDialogPreview() { valueRange = 0.0..10.0, step = 0.5, label = "Insulin", - unitLabel = "U", + unitLabel = TextRef.Literal("U"), onValueConfirm = {}, onDismiss = {} ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index a434d35edbd5..485ae71f4a0d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -16,14 +16,15 @@ import app.aaps.core.keys.decimalPlaces import app.aaps.core.keys.interfaces.DoublePreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.keys.rangeResId import app.aaps.core.keys.step -import app.aaps.core.keys.unitLabelResId -import app.aaps.core.keys.valueResId import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences +import app.aaps.core.ui.compose.isDuration +import app.aaps.core.ui.compose.rangeText import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.stringResourceOrNull +import app.aaps.core.ui.compose.unitLabel +import app.aaps.core.ui.compose.valueFormatResId /** * Composable double preference for use inside card sections. @@ -58,11 +59,11 @@ fun AdaptiveDoublePreferenceItem( val unitType = doubleKey.unitType val decimalPlaces = unitType.decimalPlaces() val step = unitType.step() - val valueFormatResId = unitType.valueResId() + val valueFormatResId = unitType.valueFormatResId() // Get unit label from UnitType (for dialog input suffix) - val unitLabelResId = unitType.unitLabelResId() - val unitLabel = unitLabelResId?.let { stringResource(it) } ?: unit + val unitLabelRef = unitType.unitLabel() ?: unit.takeIf { it.isNotEmpty() }?.let { TextRef.Literal(it) } + val unitLabelText = unitLabelRef?.let { stringResource(it) } ?: "" val valueFormat = NumberFormat.withDecimals(decimalPlaces) @@ -106,7 +107,8 @@ fun AdaptiveDoublePreferenceItem( showValue = true, valueFormatResId = valueFormatResId, valueFormat = valueFormat, - unitLabel = unitLabel, + unitLabel = unitLabelRef, + asDuration = unitType.isDuration(), dialogLabel = stringResource(effectiveTitle), dialogSummary = summary, enabled = visibility.enabled @@ -114,11 +116,11 @@ fun AdaptiveDoublePreferenceItem( } } else { // For unspecified ranges, use text field with range summary - val rangeFormatResId = unitType.rangeResId() - val summaryText = if (rangeFormatResId != null) { - stringResource(rangeFormatResId, value, doubleKey.min, doubleKey.max) + val rangeRef = unitType.rangeText(value, doubleKey.min, doubleKey.max) + val summaryText = if (rangeRef != null) { + stringResource(rangeRef) } else { - stringResource(R.string.preference_range_summary, valueFormat.format(value), unitLabel, valueFormat.format(doubleKey.min), valueFormat.format(doubleKey.max)) + stringResource(R.string.preference_range_summary, valueFormat.format(value), unitLabelText, valueFormat.format(doubleKey.min), valueFormat.format(doubleKey.max)) } TextFieldPreference( state = state, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index 095ce4a1a3d9..ab7306c0eb79 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -15,12 +15,13 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.keys.rangeResId -import app.aaps.core.keys.unitLabelResId -import app.aaps.core.keys.valueResId import app.aaps.core.ui.R +import app.aaps.core.ui.compose.isDuration +import app.aaps.core.ui.compose.rangeText import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.stringResourceOrNull +import app.aaps.core.ui.compose.unitLabel +import app.aaps.core.ui.compose.valueFormatResId /** * Composable int preference for use inside card sections. @@ -53,11 +54,11 @@ fun AdaptiveIntPreferenceItem( // Get formatting info from UnitType val unitType = intKey.unitType - val valueFormatResId = unitType.valueResId() + val valueFormatResId = unitType.valueFormatResId() // Get unit label from UnitType (for dialog input suffix) - val unitLabelResId = unitType.unitLabelResId() - val unitLabel = unitLabelResId?.let { stringResource(it) } ?: unit + val unitLabelRef = unitType.unitLabel() ?: unit.takeIf { it.isNotEmpty() }?.let { TextRef.Literal(it) } + val unitLabelText = unitLabelRef?.let { stringResource(it) } ?: "" // Get summary if available val summary = stringResourceOrNull(intKey.summary) @@ -99,7 +100,8 @@ fun AdaptiveIntPreferenceItem( valueFormatResId = valueFormatResId, formatAsInt = true, valueFormat = NumberFormat.INTEGER, - unitLabel = unitLabel, + unitLabel = unitLabelRef, + asDuration = unitType.isDuration(), dialogLabel = stringResource(effectiveTitle), dialogSummary = summary, enabled = visibility.enabled @@ -107,11 +109,11 @@ fun AdaptiveIntPreferenceItem( } } else { // For unspecified ranges, use text field with range summary - val rangeFormatResId = unitType.rangeResId() - val summaryText = if (rangeFormatResId != null) { - stringResource(rangeFormatResId, value, intKey.min, intKey.max) + val rangeRef = unitType.rangeText(value, intKey.min, intKey.max) + val summaryText = if (rangeRef != null) { + stringResource(rangeRef) } else { - stringResource(R.string.preference_range_summary, value.toString(), unitLabel, intKey.min.toString(), intKey.max.toString()) + stringResource(R.string.preference_range_summary, value.toString(), unitLabelText, intKey.min.toString(), intKey.max.toString()) } TextFieldPreference( state = state, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index fca4db6f4ff5..038c27f06d5e 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -54,19 +54,8 @@ fun AdaptivePreferenceItem( is IntPreferenceKey -> { when (key.preferenceType) { PreferenceType.LIST -> { - // Check for runtime-resolved entries first (from withEntries) - val resolved = key.resolvedEntries - if (resolved != null) { + if (key.entries.isNotEmpty()) { AdaptiveListIntPreferenceItem( - - intKey = key, - entries = resolved.values.toList(), - entryValues = resolved.keys.toList(), - visibilityContext = visibilityContext - ) - } else if (key.entries.isNotEmpty()) { - AdaptiveListIntPreferenceItem( - intKey = key, entries = key.entries.values.map { stringResource(it) }, entryValues = key.entries.keys.toList(), @@ -149,15 +138,10 @@ fun AdaptivePreferenceItem( } else { when (key.preferenceType) { PreferenceType.LIST -> { - // Check for runtime-resolved entries first (from withEntries) - val entriesMap = key.resolvedEntries - ?: key.entries.takeIf { it.isNotEmpty() }?.mapValues { (_, resId) -> stringResource(resId) } - - if (entriesMap != null) { + if (key.entries.isNotEmpty()) { AdaptiveStringListPreferenceItem( - stringKey = key, - entries = entriesMap, + entries = key.entries.mapValues { (_, ref) -> stringResource(ref) }, visibilityContext = visibilityContext ) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index 0c66e0f56058..d566d3765ea1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey @@ -64,7 +63,7 @@ fun AdaptiveUnitDoublePreferenceItem( val valueFormat = if (isMgdl) NumberFormat.INTEGER else NumberFormat.DECIMAL_1 // Get unit label from resources - short form for slider - val unitLabel = stringResource(if (isMgdl) UiR.string.mgdl else UiR.string.mmol) + val unitLabel = TextRef.Res(if (isMgdl) UiR.string.mgdl else UiR.string.mmol) // Get summary if available val summary = stringResourceOrNull(unitKey.summary) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt index b13e48d6a77d..2e3702660742 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -42,8 +43,8 @@ fun PreferenceSliderWithButtons( valueFormatResId: Int? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, - unitLabel: String = "", - unitLabelResId: Int = 0, + unitLabel: TextRef? = null, + asDuration: Boolean = false, dialogLabel: String? = null, dialogSummary: String? = null, enabled: Boolean = true, @@ -61,7 +62,7 @@ fun PreferenceSliderWithButtons( formatAsInt = formatAsInt, valueFormat = valueFormat, unitLabel = unitLabel, - unitLabelResId = unitLabelResId, + asDuration = asDuration, dialogLabel = dialogLabel, dialogSummary = dialogSummary, enabled = enabled, @@ -72,18 +73,13 @@ fun PreferenceSliderWithButtons( var showDialog by remember { mutableStateOf(false) } - val resolvedUnitLabel = when { - unitLabelResId != 0 -> stringResource(unitLabelResId) - unitLabel.isNotEmpty() -> unitLabel - else -> "" - } val displayText = if (showValue) formatSliderDisplayValue( value = value, - unitLabelResId = unitLabelResId, + unitLabel = unitLabel, valueFormatResId = valueFormatResId, formatAsInt = formatAsInt, valueFormat = valueFormat, - unitLabel = unitLabel + asDuration = asDuration ) else "" Row( @@ -111,8 +107,8 @@ fun PreferenceSliderWithButtons( step = step, label = dialogLabel, summary = dialogSummary, - unitLabel = resolvedUnitLabel, - unitLabelResId = unitLabelResId, + unitLabel = unitLabel, + asDuration = asDuration, valueFormat = valueFormat, onValueConfirm = onValueChange, onDismiss = { showDialog = false } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt index 72ab83702af5..4e9e8730e589 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt @@ -128,7 +128,6 @@ private object PreviewPreferences : Preferences { override fun isUnitDependent(key: String): Boolean = false override fun get(key: String): NonPreferenceKey? = null override fun getIfExists(key: String): NonPreferenceKey? = null - override fun getDependingOn(key: String): List = emptyList() override fun registerPreferences(keys: List) {} override fun allMatchingStrings(key: ComposedKey): List = emptyList() override fun allMatchingInts(key: ComposedKey): List = emptyList() diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt index a41e865af02a..1788e5d3d7b9 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextReplacement +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -48,7 +49,7 @@ class ValueInputDialogTest { currentValue = currentValue, valueRange = 0.0..10.0, label = "Insulin", - unitLabel = "U", + unitLabel = TextRef.Literal("U"), onValueConfirm = onValueConfirm, onDismiss = onDismiss ) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt index c4ca02f075dc..5873f2874e85 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/sharedPreferences/PreferencesImpl.kt @@ -386,11 +386,6 @@ class PreferencesImpl @Inject constructor( prefsList .find { it.key == key } - override fun getDependingOn(key: String): List = - prefsList.filterIsInstance().filter { - it.dependency?.key == key || it.negativeDependency?.key == key - } - override fun get(key: BooleanComposedNonPreferenceKey, vararg arguments: Any): Boolean = sp.getBoolean(key.composeKey(*arguments), key.defaultValue) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt index c0d1ecdf189a..8719ec9ebfe1 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.Scene +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.NumberInputRow import app.aaps.plugins.automation.R import app.aaps.plugins.automation.actions.Action @@ -148,7 +149,7 @@ fun ActionCarePortalEventEditor(a: ActionCarePortalEvent, tick: Int = 0, onChang onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) InputStringEditor( value = a.note.value, @@ -193,7 +194,7 @@ fun ActionProfileSwitchPercentEditor(a: ActionProfileSwitchPercent, tick: Int = onValueChange = { a.pct.value = it; onChange() }, valueRange = InputPercent.MIN..InputPercent.MAX, step = 5.0, - unitLabelResId = KeysR.string.units_percent + unitLabel = TextRef.Res(KeysR.string.units_percent) ) NumberInputRow( labelResId = app.aaps.core.ui.R.string.duration_label, @@ -201,7 +202,7 @@ fun ActionProfileSwitchPercentEditor(a: ActionProfileSwitchPercent, tick: Int = onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) } @@ -225,7 +226,7 @@ fun ActionRunAutotuneEditor( onValueChange = { a.daysBackRef().value = it.toInt(); onChange() }, valueRange = 1.0..30.0, step = 1.0, - unitLabelResId = KeysR.string.units_days + unitLabel = TextRef.Res(KeysR.string.units_days) ) InputWeekDayEditor(weekdays = a.daysRef(), onChange = onChange) } @@ -265,7 +266,7 @@ fun ActionStartTempTargetEditor(a: ActionStartTempTarget, tick: Int = 0, onChang valueRange = if (isMmol) Constants.TT_RANGE_MMOL else Constants.TT_RANGE_MGDL, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabelResId = if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl + unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl) ) NumberInputRow( labelResId = app.aaps.core.ui.R.string.duration_label, @@ -273,7 +274,7 @@ fun ActionStartTempTargetEditor(a: ActionStartTempTarget, tick: Int = 0, onChang onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) } diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt index 650ced0893ea..ea439c65fa93 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.automation.compose.triggers +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -122,7 +123,7 @@ fun TriggerBgEditor(t: TriggerBg, onChange: () -> Unit, tick: Int = 0) { valueRange = if (isMmol) InputBg.MMOL_MIN..InputBg.MMOL_MAX else InputBg.MGDL_MIN..InputBg.MGDL_MAX, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabelResId = if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl, + unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), compact = true ) } @@ -156,7 +157,7 @@ fun TriggerDeltaEditor(t: TriggerDelta, onChange: () -> Unit, tick: Int = 0) { valueRange = -72.0..72.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl, + unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), compact = true ) } @@ -176,7 +177,7 @@ fun TriggerCOBEditor(t: TriggerCOB, onChange: () -> Unit, tick: Int = 0) { onValueChange = { t.cob.value = it; onChange() }, valueRange = 0.0..150.0, step = 1.0, - unitLabelResId = KeysR.string.units_grams, + unitLabel = TextRef.Res(KeysR.string.units_grams), compact = true ) } @@ -197,7 +198,7 @@ fun TriggerIobEditor(t: TriggerIob, onChange: () -> Unit, tick: Int = 0) { valueRange = -20.0..20.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_insulin, + unitLabel = TextRef.Res(KeysR.string.units_insulin), compact = true ) } @@ -217,7 +218,7 @@ fun TriggerHeartRateEditor(t: TriggerHeartRate, onChange: () -> Unit, tick: Int onValueChange = { t.heartRate.value = it; onChange() }, valueRange = 30.0..250.0, step = 5.0, - unitLabelResId = R.string.automation_unit_bpm, + unitLabel = TextRef.Res(R.string.automation_unit_bpm), compact = true ) } @@ -237,7 +238,7 @@ fun TriggerAutosensValueEditor(t: TriggerAutosensValue, onChange: () -> Unit, ti onValueChange = { t.autosens.value = it; onChange() }, valueRange = 0.0..300.0, step = 1.0, - unitLabelResId = KeysR.string.units_percent, + unitLabel = TextRef.Res(KeysR.string.units_percent), compact = true ) } @@ -257,7 +258,7 @@ fun TriggerBolusAgoEditor(t: TriggerBolusAgo, onChange: () -> Unit, tick: Int = onValueChange = { t.minutesAgo.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), compact = true ) } @@ -278,7 +279,7 @@ fun TriggerCannulaAgeEditor(t: TriggerCannulaAge, onChange: () -> Unit, tick: In valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_hours, + unitLabel = TextRef.Res(KeysR.string.units_hours), compact = true ) } @@ -299,7 +300,7 @@ fun TriggerInsulinAgeEditor(t: TriggerInsulinAge, onChange: () -> Unit, tick: In valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_hours, + unitLabel = TextRef.Res(KeysR.string.units_hours), compact = true ) } @@ -319,7 +320,7 @@ fun TriggerReservoirLevelEditor(t: TriggerReservoirLevel, onChange: () -> Unit, onValueChange = { t.reservoirLevel.value = it; onChange() }, valueRange = 0.0..800.0, step = 1.0, - unitLabelResId = KeysR.string.units_insulin, + unitLabel = TextRef.Res(KeysR.string.units_insulin), compact = true ) } @@ -340,7 +341,7 @@ fun TriggerPumpBatteryAgeEditor(t: TriggerPumpBatteryAge, onChange: () -> Unit, valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_hours, + unitLabel = TextRef.Res(KeysR.string.units_hours), compact = true ) } @@ -360,7 +361,7 @@ fun TriggerPumpBatteryLevelEditor(t: TriggerPumpBatteryLevel, onChange: () -> Un onValueChange = { t.pumpBatteryLevel.value = it; onChange() }, valueRange = 0.0..100.0, step = 1.0, - unitLabelResId = KeysR.string.units_percent, + unitLabel = TextRef.Res(KeysR.string.units_percent), compact = true ) } @@ -381,7 +382,7 @@ fun TriggerSensorAgeEditor(t: TriggerSensorAge, onChange: () -> Unit, tick: Int valueRange = 0.0..720.0, step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_hours, + unitLabel = TextRef.Res(KeysR.string.units_hours), compact = true ) } @@ -406,7 +407,7 @@ fun TriggerPumpLastConnectionEditor(t: TriggerPumpLastConnection, onChange: () - onValueChange = { t.minutesAgo.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), compact = true ) } @@ -426,7 +427,7 @@ fun TriggerProfilePercentEditor(t: TriggerProfilePercent, onChange: () -> Unit, onValueChange = { t.pct.value = it; onChange() }, valueRange = InputPercent.MIN..InputPercent.MAX, step = 5.0, - unitLabelResId = KeysR.string.units_percent, + unitLabel = TextRef.Res(KeysR.string.units_percent), compact = true ) } @@ -454,7 +455,7 @@ fun TriggerTempTargetValueEditor(t: TriggerTempTargetValue, onChange: () -> Unit else Constants.TT_RANGE_MGDL, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabelResId = if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl, + unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), compact = true ) } @@ -565,7 +566,7 @@ fun TriggerLocationEditor( onValueChange = { t.distance.value = it; onChange() }, valueRange = 0.0..100000.0, step = 10.0, - unitLabelResId = R.string.automation_unit_meters + unitLabel = TextRef.Res(R.string.automation_unit_meters) ) InputLocationModeEditor( value = t.modeSelected.value, diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt index 47cac6c0cc0a..98e3ff2ecebd 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt @@ -1,7 +1,6 @@ package app.aaps.plugins.configuration.setupwizard.elements import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.resources.ResourceHelper @@ -10,6 +9,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.preference.InlineStringListPreferenceItem +import app.aaps.core.ui.compose.stringResource import javax.inject.Inject class SWRadioButton @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, rxBus: RxBus, preferences: Preferences, passwordCheck: PasswordCheck) : SWItem(aapsLogger, rh, rxBus, preferences, passwordCheck) { @@ -45,7 +45,7 @@ class SWRadioButton @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelp } } else { // Static options declared on the StringPreferenceKey. - key.entries.mapValues { (_, resId) -> stringResource(resId) } + key.entries.mapValues { (_, ref) -> stringResource(ref) } } InlineStringListPreferenceItem( stringKey = key, diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt index 39c9639b9644..df8e416f90a8 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt @@ -28,6 +28,7 @@ import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.IntKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withEntries import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -180,7 +181,7 @@ class SafetyPlugin @Inject constructor( titleResId = R.string.safety, items = listOf( StringKey.SafetyAge.withEntries( - hardLimits.ageEntryValues().zip(hardLimits.ageEntries()).associate { it.first.toString() to it.second.toString() } + hardLimits.ageEntryValues().zip(hardLimits.ageEntries()).associate { it.first.toString() to TextRef.Literal(it.second.toString()) } ), DoubleKey.SafetyMaxBolus, IntKey.SafetyMaxCarbs diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt index ea6b4d7db4aa..bfe7abf819a7 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.pump.dana.R @@ -181,7 +182,7 @@ internal fun DanaUserOptionsContent( valueRange = 5.0..240.0, step = 5.0, formatAsInt = true, - unitLabelResId = app.aaps.core.keys.R.string.units_sec, + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_sec), modifier = itemModifier ) @@ -193,7 +194,7 @@ internal fun DanaUserOptionsContent( valueRange = state.minBacklight.toDouble()..60.0, step = 1.0, formatAsInt = true, - unitLabelResId = app.aaps.core.keys.R.string.units_sec, + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_sec), modifier = itemModifier ) @@ -214,7 +215,7 @@ internal fun DanaUserOptionsContent( valueRange = 0.0..24.0, step = 1.0, formatAsInt = true, - unitLabelResId = app.aaps.core.keys.R.string.units_hours, + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_hours), modifier = itemModifier ) @@ -226,7 +227,7 @@ internal fun DanaUserOptionsContent( valueRange = 10.0..50.0, step = 10.0, formatAsInt = true, - unitLabel = "U", + unitLabel = TextRef.Literal("U"), modifier = itemModifier ) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt index be61f263bb46..849e945dbfe7 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt @@ -13,7 +13,7 @@ enum class DanaIntKey( override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -31,7 +31,7 @@ enum class DanaIntKey( defaultValue = 0, titleResId = app.aaps.core.ui.R.string.bolusspeed, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( 0 to R.string.bolus_speed_12, 1 to R.string.bolus_speed_30, 2 to R.string.bolus_speed_60 @@ -40,4 +40,5 @@ enum class DanaIntKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt index b8419a8f2f8e..3e54725e3fc9 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt @@ -13,7 +13,7 @@ enum class DiaconnIntKey( override val max: Int = Int.MAX_VALUE, private val titleResId: Int, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -31,7 +31,7 @@ enum class DiaconnIntKey( defaultValue = 5, titleResId = app.aaps.core.ui.R.string.bolusspeed, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( 1 to R.string.bolus_speed_1, 2 to R.string.bolus_speed_2, 3 to R.string.bolus_speed_3, @@ -45,4 +45,5 @@ enum class DiaconnIntKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } } diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index 823313352817..ee2ea58c9e20 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -34,7 +34,9 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.Round import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withEntries +import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.compose.icons.IcPluginEopatch import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.pump.eopatch.alarm.IAlarmManager @@ -571,11 +573,17 @@ class EopatchPumpPlugin @Inject constructor( key = "eopatch_settings", titleResId = R.string.eopatch, items = listOf( - EopatchIntKey.LowReservoirReminder.withEntries((10..50 step 5).associateWith { "$it U" }), - EopatchIntKey.ExpirationReminder.withEntries((1..24).associateWith { "$it hr" }), + // The labels used to be built as "$it U" and "$it hr", which no translator could reach. + // The unit format templates already exist and are translated, so use those. + EopatchIntKey.LowReservoirReminder.withEntries( + (10..50 step 5).associateWith { TextRef.Res(KeysR.string.units_format_insulin_int, listOf(it)) } + ), + EopatchIntKey.ExpirationReminder.withEntries( + (1..24).associateWith { TextRef.Res(KeysR.string.units_format_hours, listOf(it)) } + ), EopatchBooleanKey.BuzzerReminder ), icon = pluginDescription.icon ) -} \ No newline at end of file +} diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt index f982e9524c89..e76d91366bbc 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt @@ -13,7 +13,7 @@ enum class EopatchIntKey( override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -31,4 +31,5 @@ enum class EopatchIntKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt index 7bc2a55b7acf..e3b21fde9592 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt @@ -13,7 +13,7 @@ enum class EquilIntPreferenceKey( override val max: Int = Int.MAX_VALUE, private val titleResId: Int, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -33,7 +33,7 @@ enum class EquilIntPreferenceKey( max = 3, titleResId = R.string.equil_tone, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( 0 to R.string.equil_tone_mode_mute, 1 to R.string.equil_tone_mode_tone, 2 to R.string.equil_tone_mode_shake, @@ -43,4 +43,5 @@ enum class EquilIntPreferenceKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt index d9f54d6d1e8b..697fc79220c6 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt @@ -12,7 +12,7 @@ enum class MedtronicIntPreferenceKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val min: Int = Int.MIN_VALUE, override val max: Int = Int.MAX_VALUE, override val calculatedDefaultValue: Boolean = false, @@ -46,7 +46,7 @@ enum class MedtronicIntPreferenceKey( defaultValue = 10, titleResId = R.string.medtronic_pump_bolus_delay, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( 5 to R.string.medtronic_bolus_delay_5s, 10 to R.string.medtronic_bolus_delay_10s, 15 to R.string.medtronic_bolus_delay_15s @@ -57,5 +57,6 @@ enum class MedtronicIntPreferenceKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt index ae096e0cae8c..1592eadfef55 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt @@ -13,7 +13,7 @@ enum class MedtronicStringPreferenceKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -38,7 +38,7 @@ enum class MedtronicStringPreferenceKey( defaultValue = "", titleResId = R.string.medtronic_pump_type, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "Other (unsupported)" to R.string.medtronic_pump_type_unsupported, "512" to R.string.medtronic_pump_type_512, "712" to R.string.medtronic_pump_type_712, @@ -59,7 +59,7 @@ enum class MedtronicStringPreferenceKey( defaultValue = "medtronic_pump_frequency_us_ca", titleResId = R.string.medtronic_pump_frequency, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "medtronic_pump_frequency_us_ca" to app.aaps.pump.common.hw.rileylink.R.string.medtronic_pump_frequency_us_ca, "medtronic_pump_frequency_worldwide" to app.aaps.pump.common.hw.rileylink.R.string.medtronic_pump_frequency_worldwide ) @@ -69,7 +69,7 @@ enum class MedtronicStringPreferenceKey( defaultValue = app.aaps.pump.medtronic.defs.BatteryType.None.key, titleResId = R.string.medtronic_pump_battery_select, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( app.aaps.pump.medtronic.defs.BatteryType.None.key to R.string.medtronic_pump_battery_no, app.aaps.pump.medtronic.defs.BatteryType.Alkaline.key to R.string.medtronic_pump_battery_alkaline, app.aaps.pump.medtronic.defs.BatteryType.Lithium.key to R.string.medtronic_pump_battery_lithium, @@ -80,5 +80,6 @@ enum class MedtronicStringPreferenceKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt index 37033070bbef..d609687d7581 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt @@ -14,7 +14,7 @@ enum class MedtrumStringKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -35,7 +35,7 @@ enum class MedtrumStringKey( titleResId = R.string.alarm_setting_title, summaryResId = R.string.alarm_setting_summary, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "0" to R.string.alarm_setting_light_vibrate_beep, "1" to R.string.alarm_setting_light_vibrate, "2" to R.string.alarm_setting_light_beep, @@ -49,5 +49,6 @@ enum class MedtrumStringKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt index 7119c7c6e433..eb00024834db 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt @@ -13,7 +13,7 @@ enum class OmnipodIntPreferenceKey( override val defaultValue: Int, private val titleResId: Int, private val summaryResId: Int? = null, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val calculatedDefaultValue: Boolean = false, override val engineeringModeOnly: Boolean = false, override val defaultedBySM: Boolean = false, @@ -44,5 +44,6 @@ enum class OmnipodIntPreferenceKey( override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt index 70118f59ec6c..34622a3fad8e 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt @@ -12,7 +12,7 @@ enum class RileyLinkStringPreferenceKey( private val titleResId: Int, private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - override val entries: Map = emptyMap(), + private val entriesResIds: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -30,7 +30,7 @@ enum class RileyLinkStringPreferenceKey( defaultValue = "medtronic_pump_encoding_4b6b_rileylink", titleResId = R.string.medtronic_pump_encoding_title, preferenceType = PreferenceType.LIST, - entries = mapOf( + entriesResIds = mapOf( "medtronic_pump_encoding_4b6b_local" to R.string.medtronic_pump_encoding_4b6b_local, "medtronic_pump_encoding_4b6b_rileylink" to R.string.medtronic_pump_encoding_4b6b_rileylink ) @@ -38,5 +38,6 @@ enum class RileyLinkStringPreferenceKey( ; override val title: TextRef = TextRef.Res(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } } diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt index 76bd5923cee7..94e9646f5127 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt @@ -37,6 +37,7 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withEntries import app.aaps.core.ui.compose.icons.IcPluginVirtualPump import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -405,7 +406,7 @@ open class VirtualPumpPlugin @Inject constructor( PumpType.entries .filter { it.description != "USER" } .sortedBy { it.description } - .associate { it.description to it.description } + .associate { it.description to TextRef.Literal(it.description) } ), BooleanKey.VirtualPumpStatusUpload ), diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt index 02c0638e17cd..8c8710462a1c 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt @@ -45,13 +45,14 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.format.NumberFormat import app.aaps.core.interfaces.calibration.AddEntryResult +import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.ui.compose.navigation.labelResId import app.aaps.ui.R import app.aaps.core.ui.R as CoreUiR @@ -203,7 +204,7 @@ internal fun CalibrationDialogContent( onValueChange = onBgChange, valueRange = uiState.bgRange, step = uiState.bgStep, - unitLabel = uiState.unitLabel, + unitLabel = TextRef.Literal(uiState.unitLabel), decimalPlaces = uiState.bgDecimalPlaces ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt index c383c3fbd0e8..6dfdcedc6563 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt @@ -54,6 +54,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.CarbTimeRow import app.aaps.core.ui.compose.NumberInputRow @@ -329,7 +330,7 @@ internal fun CarbsDialogContent( valueRange = (-uiState.cobLimit).toDouble()..uiState.maxCarbs.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = stringResource(CoreUiR.string.shortgramm) + unitLabel = TextRef.Res(CoreUiR.string.shortgramm) ) // Removing carbs (negative): show the COB-bounded limit so the user understands why it can't go lower. if (uiState.carbs < 0) { @@ -355,7 +356,7 @@ internal fun CarbsDialogContent( valueRange = 0.0..uiState.maxCarbsDurationHours.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = stringResource(InterfacesR.string.shorthour), + unitLabel = TextRef.Res(InterfacesR.string.shorthour), modifier = itemModifier ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt index 794f535c81e7..24b592aadba2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt @@ -49,6 +49,7 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TE import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.DateTimeSection import app.aaps.core.ui.compose.EventTimeRow @@ -350,7 +351,7 @@ private fun BgSection( valueRange = minBg..maxBg, step = step, valueFormat = format, - unitLabel = glucoseUnits.displayLabel + unitLabel = TextRef.Literal(glucoseUnits.displayLabel) ) } } @@ -374,7 +375,7 @@ private fun DurationSection( onValueChange = onDurationChange, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), modifier = modifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt index f832c95fb23a..8507e82feeb3 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea @@ -196,7 +197,7 @@ internal fun ExtendedBolusDialogContent( valueRange = uiState.minInsulin..uiState.maxInsulin, step = uiState.extendedStep, valueFormat = NumberFormat.DECIMAL_2, - unitLabel = stringResource(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), modifier = itemModifier ) @@ -207,7 +208,7 @@ internal fun ExtendedBolusDialogContent( valueRange = uiState.extendedDurationStep..uiState.extendedMaxDuration, step = uiState.extendedDurationStep, valueFormat = NumberFormat.INTEGER, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt index a5aa35654b8e..6e142b864b35 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt @@ -54,6 +54,7 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.ICfg import app.aaps.core.data.model.TE import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.DateTimeSection import app.aaps.core.ui.compose.EventTimeRow @@ -382,7 +383,7 @@ internal fun FillDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = stringResource(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), enabled = uiState.showBolus ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt index 04e0b9676c93..9dda87d5227f 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt @@ -52,6 +52,7 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.ICfg import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.DateTimeSection import app.aaps.core.ui.compose.InsulinSelector @@ -356,7 +357,7 @@ internal fun InsulinDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = stringResource(CoreUiR.string.insulin_unit_shortname) + unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname) ) InsulinQuickAddButtons( increment1 = uiState.insulinButtonIncrement1, @@ -403,7 +404,7 @@ internal fun InsulinDialogContent( onValueChange = onTimeOffsetChange, valueRange = -12.0 * 60..12.0 * 60, step = 5.0, - unitLabelResId = KeysR.string.units_min + unitLabel = TextRef.Res(KeysR.string.units_min) ) DateTimeSection( dateString = dateString, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt index f888ad8a55f1..d64e54bf1714 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt @@ -57,6 +57,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.graph.InsulinGraphCompose import app.aaps.core.interfaces.insulin.InsulinType +import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsFab import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.MasterOfflineBanner @@ -69,7 +71,6 @@ import app.aaps.core.ui.compose.dialogs.OkDialog import app.aaps.core.ui.compose.icons.IcPluginInsulin import app.aaps.core.ui.compose.insulin.ConcentrationDropdown import app.aaps.core.ui.compose.masterEditingEnabled -import app.aaps.core.interfaces.navigation.ElementType import app.aaps.ui.R import app.aaps.ui.compose.components.ManagementCarousel import app.aaps.core.keys.R as KeysR @@ -364,7 +365,7 @@ fun InsulinManagementScreen( onValueChange = { viewModel.updateEditorPeak(it.toInt()) }, valueRange = viewModel.peakRange(), step = 1.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), enabled = editorEnabled, modifier = Modifier.fillMaxWidth() ) @@ -387,7 +388,7 @@ fun InsulinManagementScreen( valueRange = viewModel.diaRange(), step = 0.1, decimalPlaces = 1, - unitLabelResId = KeysR.string.units_hours, + unitLabel = TextRef.Res(KeysR.string.units_hours), enabled = editorEnabled, modifier = Modifier.fillMaxWidth() ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt index 9b3d145d4798..660d7dc4ef3d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt @@ -57,6 +57,7 @@ import app.aaps.core.graph.profile.buildIcRows import app.aaps.core.graph.profile.buildIsfRows import app.aaps.core.graph.profile.buildTargetRows import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow @@ -488,7 +489,7 @@ fun DefaultProfileContent( onValueChange = { onAgeChange(it.toInt()) }, valueRange = 1.0..99.0, step = 1.0, - unitLabelResId = app.aaps.core.keys.R.string.units_years + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_years) ) if (showTdd) NumberInputRow( labelResId = app.aaps.core.ui.R.string.tdd_total, @@ -496,7 +497,7 @@ fun DefaultProfileContent( onValueChange = onTddChange, valueRange = 0.0..200.0, step = 1.0, - unitLabelResId = app.aaps.core.keys.R.string.units_insulin + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_insulin) ) if (showWeight) NumberInputRow( labelResId = R.string.weight_label, @@ -504,7 +505,7 @@ fun DefaultProfileContent( onValueChange = onWeightChange, valueRange = 0.0..150.0, step = 1.0, - unitLabelResId = app.aaps.core.keys.R.string.units_kg + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_kg) ) if (showPct) NumberInputRow( labelResId = R.string.basal_pct_from_tdd_label, @@ -512,7 +513,7 @@ fun DefaultProfileContent( onValueChange = onPctChange, valueRange = 32.0..37.0, step = 1.0, - unitLabelResId = app.aaps.core.keys.R.string.units_percent + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) ) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt index 972c12dc587e..2b86e2015011 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.DateTimeSection import app.aaps.core.ui.compose.EventTimeRow @@ -272,7 +273,7 @@ fun ProfileActivationScreen( onValueChange = { percentage = it }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabelResId = app.aaps.core.keys.R.string.units_percent, + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent), modifier = itemModifier ) @@ -283,7 +284,7 @@ fun ProfileActivationScreen( onValueChange = { duration = it }, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabelResId = app.aaps.core.keys.R.string.units_min, + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_min), modifier = itemModifier ) @@ -330,7 +331,7 @@ fun ProfileActivationScreen( onValueChange = { timeshift = it }, valueRange = Constants.CPP_TIMESHIFT_RANGE, step = 1.0, - unitLabelResId = app.aaps.core.keys.R.string.units_hours + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_hours) ) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt index 0dad83c9fdf3..7d4ab19f14b1 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.configuration.Constants import app.aaps.core.interfaces.navigation.ElementCategory +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.TonalIcon @@ -430,7 +431,7 @@ private fun ProfilePresetDialog( onValueChange = { percentage = it.toInt() }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabelResId = app.aaps.core.keys.R.string.units_percent + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) ) NumberInputRow( @@ -439,7 +440,7 @@ private fun ProfilePresetDialog( onValueChange = { durationMinutes = it.toInt() }, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabelResId = app.aaps.core.keys.R.string.units_min + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_min) ) if (durationMinutes == 0) { Text( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt index f9225aad8fd8..41f28fa0894d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt @@ -10,13 +10,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Alarm import androidx.compose.material.icons.filled.PhoneAndroid import androidx.compose.material.icons.filled.Watch -import app.aaps.core.ui.compose.icons.IcBolus -import app.aaps.core.ui.compose.icons.IcCarbs -import app.aaps.core.ui.compose.icons.IcQuickwizard import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,15 +25,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import app.aaps.core.data.configuration.Constants import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.wizard.QuickWizardMode import app.aaps.core.ui.compose.LocalDateUtil import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.TimeRangePicker +import app.aaps.core.ui.compose.icons.IcBolus +import app.aaps.core.ui.compose.icons.IcCarbs +import app.aaps.core.ui.compose.icons.IcQuickwizard import app.aaps.ui.R import app.aaps.ui.compose.quickWizard.viewmodels.TrendOption import app.aaps.core.keys.R as KeysR @@ -198,7 +199,7 @@ fun QuickWizardEditor( valueRange = 0.0..maxInsulin, step = 0.05, decimalPlaces = 2, - unitLabel = stringResource(CoreR.string.insulin_unit_shortname), + unitLabel = TextRef.Res(CoreR.string.insulin_unit_shortname), modifier = Modifier.fillMaxWidth() ) } @@ -211,7 +212,7 @@ fun QuickWizardEditor( onValueChange = { onCarbsChange(it.toInt()) }, valueRange = 0.0..maxCarbs, step = 1.0, - unitLabelResId = KeysR.string.units_grams, + unitLabel = TextRef.Res(KeysR.string.units_grams), modifier = Modifier.fillMaxWidth() ) } @@ -224,7 +225,7 @@ fun QuickWizardEditor( onValueChange = { onCarbTimeChange(it.toInt()) }, valueRange = -60.0..60.0, step = 5.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), modifier = Modifier.fillMaxWidth() ) } @@ -241,113 +242,113 @@ fun QuickWizardEditor( // Calculator Options (WIZARD mode only) if (mode == QuickWizardMode.WIZARD) { - HorizontalDivider() - - // Calculator Options Section - Text( - text = stringResource(R.string.calculator_options), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - - // Use BG - SwitchRow( - label = stringResource(R.string.use_bg), - checked = useBG, - onCheckedChange = onUseBGChange - ) + HorizontalDivider() - // Use COB - SwitchRow( - label = stringResource(R.string.use_cob), - checked = useCOB, - onCheckedChange = onUseCOBChange - ) + // Calculator Options Section + Text( + text = stringResource(R.string.calculator_options), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) - // Use IOB - SwitchRow( - label = stringResource(R.string.use_iob), - checked = useIOB, - onCheckedChange = onUseIOBChange - ) + // Use BG + SwitchRow( + label = stringResource(R.string.use_bg), + checked = useBG, + onCheckedChange = onUseBGChange + ) - // Use Positive IOB Only (only visible when IOB enabled) - if (useIOB) { + // Use COB SwitchRow( - label = stringResource(R.string.overview_edit_quickwizard_use_positive_iob_only), - checked = usePositiveIOBOnly, - onCheckedChange = onUsePositiveIOBOnlyChange, - modifier = Modifier.padding(start = 16.dp) + label = stringResource(R.string.use_cob), + checked = useCOB, + onCheckedChange = onUseCOBChange ) - } - // Use Trend - Column(modifier = Modifier.fillMaxWidth()) { + // Use IOB SwitchRow( - label = stringResource(R.string.use_trend), - checked = useTrend != TrendOption.NO, - onCheckedChange = { enabled -> - onUseTrendChange(if (enabled) TrendOption.YES else TrendOption.NO) - } + label = stringResource(R.string.use_iob), + checked = useIOB, + onCheckedChange = onUseIOBChange ) - // Trend options (only visible when trend enabled) - if (useTrend != TrendOption.NO) { - Column(modifier = Modifier.padding(start = 16.dp, top = 8.dp)) { - TrendRadioButton( - label = stringResource(R.string.trend_all), - selected = useTrend == TrendOption.YES, - onClick = { onUseTrendChange(TrendOption.YES) } - ) - TrendRadioButton( - label = stringResource(R.string.trend_positive_only), - selected = useTrend == TrendOption.POSITIVE_ONLY, - onClick = { onUseTrendChange(TrendOption.POSITIVE_ONLY) } - ) - TrendRadioButton( - label = stringResource(R.string.trend_negative_only), - selected = useTrend == TrendOption.NEGATIVE_ONLY, - onClick = { onUseTrendChange(TrendOption.NEGATIVE_ONLY) } - ) + + // Use Positive IOB Only (only visible when IOB enabled) + if (useIOB) { + SwitchRow( + label = stringResource(R.string.overview_edit_quickwizard_use_positive_iob_only), + checked = usePositiveIOBOnly, + onCheckedChange = onUsePositiveIOBOnlyChange, + modifier = Modifier.padding(start = 16.dp) + ) + } + + // Use Trend + Column(modifier = Modifier.fillMaxWidth()) { + SwitchRow( + label = stringResource(R.string.use_trend), + checked = useTrend != TrendOption.NO, + onCheckedChange = { enabled -> + onUseTrendChange(if (enabled) TrendOption.YES else TrendOption.NO) + } + ) + // Trend options (only visible when trend enabled) + if (useTrend != TrendOption.NO) { + Column(modifier = Modifier.padding(start = 16.dp, top = 8.dp)) { + TrendRadioButton( + label = stringResource(R.string.trend_all), + selected = useTrend == TrendOption.YES, + onClick = { onUseTrendChange(TrendOption.YES) } + ) + TrendRadioButton( + label = stringResource(R.string.trend_positive_only), + selected = useTrend == TrendOption.POSITIVE_ONLY, + onClick = { onUseTrendChange(TrendOption.POSITIVE_ONLY) } + ) + TrendRadioButton( + label = stringResource(R.string.trend_negative_only), + selected = useTrend == TrendOption.NEGATIVE_ONLY, + onClick = { onUseTrendChange(TrendOption.NEGATIVE_ONLY) } + ) + } } } - } - // Use Super Bolus (only if enabled in preferences) - if (showSuperBolusOption) { + // Use Super Bolus (only if enabled in preferences) + if (showSuperBolusOption) { + SwitchRow( + label = stringResource(R.string.overview_edit_quickwizard_superbolus), + checked = useSuperBolus, + onCheckedChange = onUseSuperBolusChange + ) + } + + // Use Temp Target SwitchRow( - label = stringResource(R.string.overview_edit_quickwizard_superbolus), - checked = useSuperBolus, - onCheckedChange = onUseSuperBolusChange + label = stringResource(R.string.use_temp_target), + checked = useTempTarget, + onCheckedChange = onUseTempTargetChange ) - } - // Use Temp Target - SwitchRow( - label = stringResource(R.string.use_temp_target), - checked = useTempTarget, - onCheckedChange = onUseTempTargetChange - ) - - // Alarm - SwitchRow( - label = stringResource(R.string.use_alarm), - checked = useAlarm, - onCheckedChange = onUseAlarmChange, - icon = Icons.Default.Alarm - ) + // Alarm + SwitchRow( + label = stringResource(R.string.use_alarm), + checked = useAlarm, + onCheckedChange = onUseAlarmChange, + icon = Icons.Default.Alarm + ) - HorizontalDivider() + HorizontalDivider() - // Percentage - NumberInputRow( - labelResId = KeysR.string.pref_title_bolus_percentage, - value = percentage.toDouble(), - onValueChange = { onPercentageChange(it.toInt()) }, - valueRange = Constants.WIZARD_PERCENTAGE_RANGE, - step = 5.0, - unitLabelResId = KeysR.string.units_percent, - modifier = Modifier.fillMaxWidth() - ) + // Percentage + NumberInputRow( + labelResId = KeysR.string.pref_title_bolus_percentage, + value = percentage.toDouble(), + onValueChange = { onPercentageChange(it.toInt()) }, + valueRange = Constants.WIZARD_PERCENTAGE_RANGE, + step = 5.0, + unitLabel = TextRef.Res(KeysR.string.units_percent), + modifier = Modifier.fillMaxWidth() + ) } // end WIZARD-only section @@ -377,54 +378,54 @@ fun QuickWizardEditor( // Extended Carbs Section (WIZARD and CARBS modes) if (mode != QuickWizardMode.INSULIN) { - HorizontalDivider() - SwitchRow( - label = stringResource(R.string.additional_ecarbs), - checked = useEcarbs, - onCheckedChange = onUseEcarbsChange - ) + HorizontalDivider() + SwitchRow( + label = stringResource(R.string.additional_ecarbs), + checked = useEcarbs, + onCheckedChange = onUseEcarbsChange + ) - if (useEcarbs) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Time offset - NumberInputRow( - labelResId = R.string.time_offset, - value = time.toDouble(), - onValueChange = { onTimeChange(it.toInt()) }, - valueRange = (-7 * 24 * 60).toDouble()..(12 * 60).toDouble(), - step = 5.0, - unitLabelResId = KeysR.string.units_min, - modifier = Modifier.fillMaxWidth() - ) + if (useEcarbs) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Time offset + NumberInputRow( + labelResId = R.string.time_offset, + value = time.toDouble(), + onValueChange = { onTimeChange(it.toInt()) }, + valueRange = (-7 * 24 * 60).toDouble()..(12 * 60).toDouble(), + step = 5.0, + unitLabel = TextRef.Res(KeysR.string.units_min), + modifier = Modifier.fillMaxWidth() + ) - // Duration - NumberInputRow( - labelResId = CoreR.string.duration, - value = duration.toDouble(), - onValueChange = { onDurationChange(it.toInt()) }, - valueRange = 0.0..10.0, - step = 1.0, - unitLabelResId = KeysR.string.units_hours, - modifier = Modifier.fillMaxWidth() - ) + // Duration + NumberInputRow( + labelResId = CoreR.string.duration, + value = duration.toDouble(), + onValueChange = { onDurationChange(it.toInt()) }, + valueRange = 0.0..10.0, + step = 1.0, + unitLabel = TextRef.Res(KeysR.string.units_hours), + modifier = Modifier.fillMaxWidth() + ) - // Additional carbs - NumberInputRow( - labelResId = R.string.ecarbs_additional, - value = carbs2.toDouble(), - onValueChange = { onCarbs2Change(it.toInt()) }, - valueRange = 0.0..maxCarbs, - step = 1.0, - unitLabelResId = KeysR.string.units_grams, - modifier = Modifier.fillMaxWidth() - ) + // Additional carbs + NumberInputRow( + labelResId = R.string.ecarbs_additional, + value = carbs2.toDouble(), + onValueChange = { onCarbs2Change(it.toInt()) }, + valueRange = 0.0..maxCarbs, + step = 1.0, + unitLabel = TextRef.Res(KeysR.string.units_grams), + modifier = Modifier.fillMaxWidth() + ) + } } - } } // end non-INSULIN section } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt index 0c41f39a8b78..bd0325f25191 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt @@ -41,6 +41,7 @@ import app.aaps.core.data.model.SceneAction import app.aaps.core.data.model.TE import app.aaps.core.data.model.TT import app.aaps.core.data.model.TTPreset +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.NumberInputRow @@ -144,7 +145,7 @@ internal fun ProfileSwitchEditor( onValueChange = { onUpdate(action.copy(percentage = it.toInt())) }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabelResId = app.aaps.core.keys.R.string.units_percent + unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) ) } @@ -197,8 +198,8 @@ internal fun RunningModeEditor( @Composable internal fun loopModeDisplayName(mode: RM.Mode): String = when (mode) { - RM.Mode.CLOSED_LOOP -> stringResource(R.string.closedloop) - RM.Mode.CLOSED_LOOP_LGS -> stringResource(R.string.lowglucosesuspend) + RM.Mode.CLOSED_LOOP -> stringResource(R.string.closedloop) + RM.Mode.CLOSED_LOOP_LGS -> stringResource(R.string.lowglucosesuspend) RM.Mode.OPEN_LOOP -> stringResource(R.string.openloop) RM.Mode.DISABLED_LOOP -> stringResource(R.string.disableloop) RM.Mode.SUSPENDED_BY_USER -> stringResource(R.string.suspendloop) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt index 57df0da1f1d4..6a95791adfc2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea @@ -193,7 +194,7 @@ internal fun TempBasalDialogContent( valueRange = 0.0..uiState.maxTempPercent, step = uiState.tempPercentStep, valueFormat = NumberFormat.INTEGER, - unitLabel = "%", + unitLabel = TextRef.Literal("%"), modifier = itemModifier ) } else { @@ -204,7 +205,7 @@ internal fun TempBasalDialogContent( valueRange = 0.0..uiState.maxTempAbsolute, step = uiState.tempAbsoluteStep, valueFormat = NumberFormat.DECIMAL_2, - unitLabel = stringResource(CoreUiR.string.profile_ins_units_per_hour), + unitLabel = TextRef.Res(CoreUiR.string.profile_ins_units_per_hour), modifier = itemModifier ) } @@ -217,7 +218,7 @@ internal fun TempBasalDialogContent( valueRange = uiState.tempDurationStep..uiState.tempMaxDuration, step = uiState.tempDurationStep, valueFormat = NumberFormat.INTEGER, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt index 0da8da400f71..1bdaed432bc0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt @@ -27,6 +27,7 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TTPreset import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalDateUtil import app.aaps.core.ui.compose.NumberInputRow @@ -114,7 +115,7 @@ fun TempTargetEditor( valueRange = targetRange, step = targetStep, valueFormat = if (units == GlucoseUnit.MGDL) NumberFormat.INTEGER else NumberFormat.DECIMAL_1, - unitLabel = units.displayLabel, + unitLabel = TextRef.Literal(units.displayLabel), modifier = Modifier.fillMaxWidth() ) @@ -125,7 +126,7 @@ fun TempTargetEditor( onValueChange = { onDurationChange((it * 60000L).toLong()) }, valueRange = Constants.ACTION_DURATION, step = 5.0, - unitLabelResId = KeysR.string.units_min, + unitLabel = TextRef.Res(KeysR.string.units_min), modifier = Modifier.fillMaxWidth() ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt index c1251372a0eb..17ccffe57488 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.banner.WarningBanner @@ -229,7 +230,7 @@ internal fun TreatmentDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = stringResource(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), modifier = itemModifier ) @@ -240,7 +241,7 @@ internal fun TreatmentDialogContent( valueRange = 0.0..uiState.maxCarbs.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = stringResource(CoreUiR.string.shortgramm), + unitLabel = TextRef.Res(CoreUiR.string.shortgramm), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt index 643127eee116..c674136c0250 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt @@ -76,6 +76,7 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.format.NumberFormat import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.interfaces.utils.DecimalFormatter +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.CarbTimeRow import app.aaps.core.ui.compose.NumberInputRow @@ -616,7 +617,7 @@ internal fun WizardDialogContent( onValueChange = onCarbsChange, valueRange = 0.0..uiState.maxCarbs.toDouble(), step = 1.0, - unitLabel = "g" + unitLabel = TextRef.Literal("g") ) QuickAddButtons( increment1 = uiState.carbsButtonIncrement1, @@ -707,7 +708,7 @@ internal fun WizardDialogContent( onValueChange = onDirectCorrectionChange, valueRange = -uiState.maxBolus..uiState.maxBolus, step = uiState.bolusStep, - unitLabel = stringResource(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), decimalPlaces = 2, modifier = itemModifier ) @@ -776,7 +777,7 @@ internal fun WizardDialogContent( onValueChange = onBgChange, valueRange = uiState.bgRange, step = uiState.bgStep, - unitLabel = unitsLabel, + unitLabel = TextRef.Literal(unitsLabel), decimalPlaces = if (uiState.isMgdl) 0 else 1 ) } @@ -823,7 +824,7 @@ internal fun WizardDialogContent( onValueChange = onPercentageChange, valueRange = Constants.WIZARD_PERCENTAGE_RANGE, step = 5.0, - unitLabel = "%", + unitLabel = TextRef.Literal("%"), decimalPlaces = 0 ) } diff --git a/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt b/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt index 75381937b9be..3aad933aa290 100644 --- a/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt +++ b/wear/src/main/kotlin/app/aaps/wear/sharedPreferences/PreferencesImpl.kt @@ -256,11 +256,6 @@ class PreferencesImpl @Inject constructor( override fun observe(key: StringComposedNonPreferenceKey, vararg arguments: Any): StateFlow = stringFlows.getOrPut(key.composeKey(*arguments)) { MutableStateFlow(get(key, *arguments)) } - override fun getDependingOn(key: String): List = - prefsList.filterIsInstance().filter { - it.dependency?.key == key || it.negativeDependency?.key == key - } - override fun registerPreferences(keys: List) { prefsList.addAll(keys) } diff --git a/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt b/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt index 7a8e0c717d85..0af07efebfb5 100644 --- a/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/sharedPreferences/PreferencesImplTest.kt @@ -107,18 +107,6 @@ internal class PreferencesImplTest { assertThat(sut.isExportableKey("totally_unrelated_key")).isFalse() } - @Test - fun getDependingOnReturnsDependencyChildren() { - val wearControlDependents = sut.getDependingOn("wearcontrol") - assertThat(wearControlDependents).containsAtLeast( - BooleanKey.WearWizardBg, - BooleanKey.WearWizardTt, - BooleanKey.WearWizardTrend, - BooleanKey.WearWizardCob, - BooleanKey.WearWizardIob - ) - } - @Test fun getAllPreferenceKeysReturnsOnlyPreferenceKeysAndExcludesNonPreferenceEnums() { val all = sut.getAllPreferenceKeys() From 6fdb924e6b1b195f09428ddec3d8b458b13fcace Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 8 Aug 2026 20:43:25 +0200 Subject: [PATCH 015/146] :core:keys String migration --- .../interfaces/protection/PasswordCheck.kt | 27 +++++++ .../interfaces/resources/ResourceHelper.kt | 6 +- .../kotlin/app/aaps/core/keys/BooleanKey.kt | 4 +- .../kotlin/app/aaps/core/keys/DoubleKey.kt | 4 +- .../main/kotlin/app/aaps/core/keys/IntKey.kt | 6 +- .../kotlin/app/aaps/core/keys/IntentKey.kt | 4 +- .../kotlin/app/aaps/core/keys/StringKey.kt | 19 +++-- .../app/aaps/core/keys/UnitDoubleKey.kt | 4 +- .../core/keys/interfaces/IntPreferenceKey.kt | 2 +- .../keys/interfaces/StringPreferenceKey.kt | 2 +- .../app/aaps/core/keys/interfaces/TextRef.kt | 23 +++--- .../src/main/res/values-bg-rBG/strings.xml | 41 ---------- .../src/main/res/values-cs-rCZ/strings.xml | 41 ---------- .../src/main/res/values-es-rES/strings.xml | 41 ---------- .../src/main/res/values-fr-rFR/strings.xml | 41 ---------- .../src/main/res/values-it-rIT/strings.xml | 41 ---------- .../src/main/res/values-nb-rNO/strings.xml | 41 ---------- .../src/main/res/values-ro-rRO/strings.xml | 41 ---------- .../src/main/res/values-sk-rSK/strings.xml | 41 ---------- .../src/main/res/values-vi-rVN/strings.xml | 41 ---------- .../src/main/res/values-zh-rCN/strings.xml | 41 ---------- .../src/main/res/values-zh-rTW/strings.xml | 41 ---------- core/keys/src/main/res/values/strings.xml | 41 ---------- .../app/aaps/core/ui/compose/CarbTimeRow.kt | 3 +- .../aaps/core/ui/compose/NumberInputRow.kt | 45 ++++++++++- .../core/ui/compose/NumberInputRowPreviews.kt | 7 +- .../ui/compose/SliderWithButtonsPreviews.kt | 4 +- .../aaps/core/ui/compose/TextRefResource.kt | 6 +- .../app/aaps/core/ui/compose/UnitTypeText.kt | 80 +++++++++---------- .../AdaptiveMasterPasswordPreference.kt | 5 +- .../preference/AdaptiveStringPreference.kt | 2 +- .../AdaptiveUnitDoublePreference.kt | 2 +- .../CollapsibleCardSectionContentPreviews.kt | 2 +- .../preference/PreferenceSubScreenDef.kt | 4 +- .../app/aaps/core/ui/locale/LocaleHelper.kt | 2 +- .../app/aaps/core/ui/search/SearchableItem.kt | 8 +- .../ui/src/main/res/values-bg-rBG/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-cs-rCZ/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-es-rES/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-fr-rFR/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-it-rIT/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-nb-rNO/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-ro-rRO/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-sk-rSK/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-vi-rVN/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-zh-rCN/strings.xml | 41 ++++++++++ .../ui/src/main/res/values-zh-rTW/strings.xml | 41 ++++++++++ core/ui/src/main/res/values/strings.xml | 41 ++++++++++ .../MorePreferenceComponentsTest.kt | 4 +- .../maintenance/ImportExportPrefsImpl.kt | 6 +- .../protection/BiometricCheck.kt | 6 +- .../protection/PasswordCheckImpl.kt | 29 ++++++- .../app/aaps/plugins/aps/keys/ApsIntentKey.kt | 4 +- .../compose/actions/ActionEditors.kt | 14 ++-- .../compose/triggers/TriggerEditors.kt | 36 ++++----- .../configuration/setupwizard/SWDefinition.kt | 6 +- .../setupwizard/SWEventListener.kt | 10 ++- .../configuration/setupwizard/SWScreen.kt | 18 ++--- .../setupwizard/elements/SWEditIntNumber.kt | 2 +- .../setupwizard/elements/SWEditNumber.kt | 2 +- .../elements/SWEditNumberWithUnits.kt | 2 +- .../setupwizard/elements/SWEditString.kt | 2 +- .../setupwizard/elements/SWEditUrl.kt | 2 +- .../setupwizard/elements/SWHtmlLink.kt | 6 +- .../setupwizard/elements/SWInfoText.kt | 6 +- .../setupwizard/elements/SWItem.kt | 8 +- .../setupwizard/elements/SWRadioButton.kt | 2 +- .../objectives/objectives/Objective0.kt | 2 +- .../source/instara/InstaraBooleanKey.kt | 4 +- .../sync/garmin/keys/GarminBooleanKey.kt | 2 +- .../plugins/sync/garmin/keys/GarminIntKey.kt | 2 +- .../sync/garmin/keys/GarminStringKey.kt | 2 +- .../sync/smsCommunicator/keys/SmsIntentKey.kt | 4 +- .../sync/tidepool/keys/TidepoolBooleanKey.kt | 4 +- .../plugins/sync/xdrip/keys/XdripIntentKey.kt | 4 +- .../pump/combov2/keys/ComboBooleanKey.kt | 2 +- .../pump/combov2/keys/ComboIntKey.kt | 2 +- .../dana/compose/DanaUserOptionsScreen.kt | 7 +- .../app/aaps/pump/dana/keys/DanaBooleanKey.kt | 4 +- .../app/aaps/pump/dana/keys/DanaIntKey.kt | 4 +- .../app/aaps/pump/dana/keys/DanaIntentKey.kt | 2 +- .../pump/diaconn/keys/DiaconnBooleanKey.kt | 4 +- .../aaps/pump/diaconn/keys/DiaconnIntKey.kt | 4 +- .../pump/diaconn/keys/DiaconnIntentKey.kt | 2 +- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 6 +- .../pump/eopatch/keys/EopatchBooleanKey.kt | 2 +- .../aaps/pump/eopatch/keys/EopatchIntKey.kt | 4 +- .../equil/keys/EquilBooleanPreferenceKey.kt | 2 +- .../pump/equil/keys/EquilIntPreferenceKey.kt | 4 +- .../pump/insight/keys/InsightBooleanKey.kt | 4 +- .../aaps/pump/insight/keys/InsightIntKey.kt | 2 +- .../keys/MedtronicBooleanPreferenceKey.kt | 4 +- .../keys/MedtronicIntPreferenceKey.kt | 6 +- .../keys/MedtronicStringPreferenceKey.kt | 6 +- .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 20 ++--- .../pump/medtrum/keys/MedtrumBooleanKey.kt | 4 +- .../aaps/pump/medtrum/keys/MedtrumIntKey.kt | 4 +- .../pump/medtrum/keys/MedtrumStringKey.kt | 6 +- .../common/keys/DashBooleanPreferenceKey.kt | 2 +- .../keys/OmnipodBooleanPreferenceKey.kt | 4 +- .../common/keys/OmnipodIntPreferenceKey.kt | 6 +- .../eros/keys/ErosBooleanPreferenceKey.kt | 2 +- .../keys/RileyLinkStringPreferenceKey.kt | 6 +- .../keys/RileylinkBooleanPreferenceKey.kt | 4 +- .../compose/carbsDialog/CarbsDialogScreen.kt | 4 +- .../ui/compose/careDialog/CareDialogScreen.kt | 3 +- .../ExtendedBolusDialogScreen.kt | 5 +- .../ui/compose/fillDialog/FillDialogScreen.kt | 2 +- .../insulinDialog/InsulinDialogScreen.kt | 5 +- .../InsulinManagementScreen.kt | 5 +- .../compose/maintenance/MaintenanceDialogs.kt | 5 +- .../profileHelper/ProfileHelperScreen.kt | 9 ++- .../ProfileActivationScreen.kt | 7 +- .../quickLaunch/QuickLaunchConfigScreen.kt | 5 +- .../compose/quickWizard/QuickWizardEditor.kt | 18 ++--- .../aaps/ui/compose/scenes/ActionEditors.kt | 2 +- .../tempBasalDialog/TempBasalDialogScreen.kt | 5 +- .../ui/compose/tempTarget/TempTargetEditor.kt | 3 +- .../treatmentDialog/TreatmentDialogScreen.kt | 4 +- .../compose/treatments/ExtendedBolusScreen.kt | 3 +- .../compose/treatments/ProfileSwitchScreen.kt | 3 +- .../ui/compose/treatments/TempBasalScreen.kt | 3 +- .../ui/compose/treatments/TempTargetScreen.kt | 3 +- .../wizardDialog/WizardDialogScreen.kt | 2 +- .../app/aaps/ui/search/BuiltInSearchables.kt | 2 +- 125 files changed, 889 insertions(+), 782 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt index a52a81de711c..17551dcb471b 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt @@ -3,6 +3,7 @@ package app.aaps.core.interfaces.protection import android.content.Context import androidx.annotation.StringRes import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef interface PasswordCheck { @@ -39,4 +40,30 @@ interface PasswordCheck { @StringRes passwordWarning: Int?, ok: ((String) -> Unit)?, cancel: (() -> Unit)? = null ) + /** + * [TextRef] variants of the three calls above. + * + * Needed because the label often comes from a preference key, and keys in a multiplatform module + * have no Android resource id to pass. The `Int` versions stay for callers that name their own + * module's `R.string.x`. + */ + fun queryPassword( + context: Context, + label: TextRef, + preference: StringPreferenceKey, + ok: ((String) -> Unit)?, + cancel: (() -> Unit)? = null, + fail: (() -> Unit)? = null, + pinInput: Boolean = false + ) + + fun setPassword( + context: Context, + label: TextRef, + preference: StringPreferenceKey, + ok: ((String) -> Unit)? = null, + cancel: (() -> Unit)? = null, + clear: (() -> Unit)? = null, + pinInput: Boolean = false + ) } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index 6d7e53135fd8..abc8b173ab2b 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -27,11 +27,11 @@ interface ResourceHelper { * * Every non-Compose reader of a preference title goes through here, which is the point: when a * module later moves its strings out of `res/values`, only this one method has to learn about - * the new form of [TextRef.Res]. + * the new form of [TextRef.AndroidRes]. */ fun gs(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text - is TextRef.Res -> + is TextRef.AndroidRes -> if (ref.args.isEmpty()) gs(ref.id) else gs(ref.id, *ref.args.toTypedArray()) } @@ -39,7 +39,7 @@ interface ResourceHelper { /** Same, but always in English - used to build the search index. */ fun gsNotLocalised(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text - is TextRef.Res -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) + is TextRef.AndroidRes -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) } @ColorInt fun gc(@ColorRes id: Int): Int diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt index 4705df0c3b15..748dadc226dc 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt @@ -261,6 +261,6 @@ enum class BooleanKey( ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt index c49d41713345..45e7bb97fe36 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt @@ -347,6 +347,6 @@ enum class DoubleKey( ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt index fa29e0f77fe1..9fa5e405a18a 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt @@ -454,7 +454,7 @@ enum class IntKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt index 6b6303f9a6e1..e947b1ef2c37 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt @@ -33,6 +33,6 @@ enum class IntentKey( // properties. The `;` is what separates the - empty - constant list from the members. ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt index 37485de1a74d..6f89516ef154 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt @@ -17,6 +17,12 @@ enum class StringKey( private val summaryResId: Int? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, private val entriesResIds: Map = emptyMap(), + /** + * Entry labels that are not a resource in this module. Only the units preference needs this: + * "mg/dL" and "mmol/L" read the same in every language, and the `units_*` strings themselves + * now live in `:core:ui`, which this module cannot depend on. + */ + private val entriesLiterals: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, override val showInNsClientMode: Boolean = true, @@ -39,9 +45,9 @@ enum class StringKey( defaultValue = "mg/dl", titleResId = R.string.pref_title_units, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - "mg/dl" to R.string.units_mgdl, - "mmol" to R.string.units_mmol + entriesLiterals = mapOf( + "mg/dl" to "mg/dL", + "mmol" to "mmol/L" ), sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), @@ -203,7 +209,8 @@ enum class StringKey( ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = + entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + entriesLiterals.mapValues { TextRef.Literal(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt index a2a30d4c7e2f..a2e6d5d27a15 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt @@ -41,6 +41,6 @@ enum class UnitDoubleKey( ) ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt index 796c7bfff0a5..6e377f009b49 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt @@ -51,7 +51,7 @@ class IntKeyWithEntries( * Use this when the entries are only known at run time - a generated range, or a list that depends * on the connected device. * - * @param entries Map of stored value -> label. Use [TextRef.Res] with arguments for anything the + * @param entries Map of stored value -> label. Use [TextRef.AndroidRes] with arguments for anything the * user reads, so it stays translatable; [TextRef.Literal] only for text that is genuinely not a * resource, such as a device name. * @return A new IntPreferenceKey with the entries attached diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt index 6829f6132110..a93e546ee95a 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt @@ -48,7 +48,7 @@ class StringKeyWithEntries( * Use this when the entries are only known at run time - for example a list that depends on the * connected device, or on values computed from another setting. * - * @param entries Map of stored value -> label. Use [TextRef.Res] with arguments for anything the + * @param entries Map of stored value -> label. Use [TextRef.AndroidRes] with arguments for anything the * user reads, so it stays translatable; [TextRef.Literal] only for text that is genuinely not a * resource, such as a device name. * @return A new StringPreferenceKey with the entries attached diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt index d96e4b332007..54de5728b019 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt @@ -10,11 +10,10 @@ package app.aaps.core.keys.interfaces * * ### Do not persist it * - * A [TextRef] is meaningful **only inside one running process**. [Res.id] must never be written to - * preferences, to the database, to a Nightscout document, to a wear message or into a crash report - * as a number, because the same number means different things on different platforms and will change - * again when a module moves its strings to `commonMain`. Persist the preference `key` instead, which - * is a stable string. + * A [TextRef] is meaningful **only inside one running process**. [AndroidRes.id] must never be + * written to preferences, to the database, to a Nightscout document, to a wear message or into a + * crash report as a number, because the same number means different things on different builds. + * Persist the preference `key` instead, which is a stable string. * * ### Why an Int and not a string name * @@ -26,21 +25,17 @@ package app.aaps.core.keys.interfaces sealed interface TextRef { /** - * Text that lives in a resource table. + * A string from an Android `R.string.*` table, in a module that still owns AAPT resources. * - * [id] is deliberately opaque: - * - **positive** - an Android resource id from `R.string.*`, used directly. This is the only - * form that exists today. - * - **negative** - a token generated when a module moves its strings to - * `commonMain/composeResources`; the resolver turns it into an index into that module's - * platform table. Android resource ids are always positive (`0x7f……`), so the two forms can - * coexist and modules can migrate one at a time rather than all at once. + * Most of the app is still this form, and that is fine - a module only needs to change when it + * itself becomes multiplatform. The two resource forms coexist so modules can migrate one at a + * time rather than all at once. * * [args] are format arguments, in the order the format string expects. They are not checked at * compile time - no worse than `stringResource(id, a, b)` today, but the arguments now travel * further from the format string, so a mismatch shows up when the text is built. */ - data class Res(val id: Int, val args: List = emptyList()) : TextRef + data class AndroidRes(val id: Int, val args: List = emptyList()) : TextRef /** * Text that is only known at run time - a scanned pump name, a wiki page title, a user label. diff --git a/core/keys/src/main/res/values-bg-rBG/strings.xml b/core/keys/src/main/res/values-bg-rBG/strings.xml index 83c692bd0c71..47ce78ccbddf 100644 --- a/core/keys/src/main/res/values-bg-rBG/strings.xml +++ b/core/keys/src/main/res/values-bg-rBG/strings.xml @@ -235,48 +235,8 @@ Дете Единици - мг/дл - ммол/л - Ед - Е/ч - мин - сек - ч - дни - гр - кг - г - % - %1$d гр - %1$d мин - %1$d сек - %1$d ч - %1$.0f ч - %1$d дни - %1$d%% - %1$.1f Ед - %1$d Ед - %1$.1f Ед/ч - %1$s мг/дл - %1$.1f - %1$.2f - %1$.3f - %1$d гр (%2$d - %3$d) - %1$d мин (%2$d - %3$d) - %1$d сек (%2$d - %3$d) - %1$d ч (%2$d - %3$d) - %1$.0f ч (%2$.0f - %3$.0f) - %1$d дни (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d Ед (%2$d - %3$d) - %1$.1f Ед (%2$.1f - %3$.1f) - %1$.1f Е/ч (%2$.1f - %3$.1f) - %1$s мг/дл (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Език Системния English @@ -417,5 +377,4 @@ Стойността на КЗ под която инсулина се спира. По подразбиране се използва стандарните стойности. Потребителят може да настройва стойности между 60mg/dl (3.3ммол/л) и 100mg/dl(5,5ммол/л). Стойности под 65/3.6 ще доведат до използване на стандартния модел. - Диапазон на визуализация diff --git a/core/keys/src/main/res/values-cs-rCZ/strings.xml b/core/keys/src/main/res/values-cs-rCZ/strings.xml index 43ea2bc7274b..52955dd9e678 100644 --- a/core/keys/src/main/res/values-cs-rCZ/strings.xml +++ b/core/keys/src/main/res/values-cs-rCZ/strings.xml @@ -235,48 +235,8 @@ Dítě Jednotky - mg/dL - mmol/L - U - U/h - min - s - h - dní - g - kg - r - % - %1$d g - %1$d min - %1$d s - %1$d h - %1$.0f h - %1$d dní - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d s (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d dní (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Jazyk Výchozí systémový Angličtina @@ -417,5 +377,4 @@ Hodnota glykémie, pod kterou je pozastaven inzulín. Výchozí hodnota používá standardní model cíle. Uživatel může nastavit hodnotu mezi 60 mg/dl (3,3 mmol/l) a 100 mg/dl (5,5 mmol/l). Hodnoty pod 65/3,6 způsobí použití výchozího modelu - Rozsah pro vizualizaci diff --git a/core/keys/src/main/res/values-es-rES/strings.xml b/core/keys/src/main/res/values-es-rES/strings.xml index ff8817386211..96e6c97bce80 100644 --- a/core/keys/src/main/res/values-es-rES/strings.xml +++ b/core/keys/src/main/res/values-es-rES/strings.xml @@ -235,48 +235,8 @@ Menor Unidades - mg/dL - mmol/L - U - U/h - min - seg - h - días - g - kg - a - % - %1$d g - %1$d min - %1$d seg - %1$d h - %1$.0f h - %1$d días - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d seg (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d días (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Idioma Predeterminado del sistema Inglés @@ -419,5 +379,4 @@ Conceptos cl Valor de glucosa por debajo del cual se suspende la insulina. El valor predeterminado utiliza el modelo de objetivo estándar. El usuario puede establecer un valor entre 60 mg/dl (3.3 mmol/l) y 100 mg/dl (5.5 mmol/l). Los valores inferiores a 65/3.6 darán como resultado el uso del modelo predeterminado. - Rango para visualización diff --git a/core/keys/src/main/res/values-fr-rFR/strings.xml b/core/keys/src/main/res/values-fr-rFR/strings.xml index 73e825e8c9a3..5fbbc20865a0 100644 --- a/core/keys/src/main/res/values-fr-rFR/strings.xml +++ b/core/keys/src/main/res/values-fr-rFR/strings.xml @@ -235,48 +235,8 @@ Enfant Unités - mg/dL - mmol/L - U - U/h - m - s - h - jours - g - kg - a - % - %1$d g - %1$d m - %1$d s - %1$d h - %1$.0f h - %1$d jours - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d m (%2$d - %3$d) - %1$d s (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d jours (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Langue Système Anglais @@ -417,5 +377,4 @@ Valeur glycémique au-dessous de laquelle l\'injection de l\'insuline est suspendu. La valeur par défaut utilise le modèle standard de la cible. L\'utilisateur peut choisir entre 60mg/dl (3.3mmol/l) et 100mg/dl (5.5mmol/l). Les valeurs au-dessous de 65/3.6 déclenchent l\'utilisation du modèle standard - Fourchette de visualisation diff --git a/core/keys/src/main/res/values-it-rIT/strings.xml b/core/keys/src/main/res/values-it-rIT/strings.xml index faaff64325af..797e6a9b4e80 100644 --- a/core/keys/src/main/res/values-it-rIT/strings.xml +++ b/core/keys/src/main/res/values-it-rIT/strings.xml @@ -235,48 +235,8 @@ Bambino Unità - mg/dL - mmol/L - U - U/h - min - sec - h - giorni - g - kg - y - % - %1$d g - %1$d min - %1$d sec - %1$d h - %1$.0f h - %1$d giorni - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d sec (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d giorni (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Lingua Predefinito di sistema English @@ -417,5 +377,4 @@ Glicemia sotto la quale l\'erogazione d\'insulina è sospesa. Il valore predefinito utilizza il modello target standard. L\'utente può impostare un valore compreso tra 60 mg/dl (3.3mmol/l) e 100mg/dl (5.5mmol/l). Con valori inferiori a 65 mg/dl (3.6mmol/l) viene usato il modello predefinito - Intervallo di visualizzazione diff --git a/core/keys/src/main/res/values-nb-rNO/strings.xml b/core/keys/src/main/res/values-nb-rNO/strings.xml index bc2db387ab73..79a0a4d25f15 100644 --- a/core/keys/src/main/res/values-nb-rNO/strings.xml +++ b/core/keys/src/main/res/values-nb-rNO/strings.xml @@ -235,48 +235,8 @@ Barn Enheter - mg/dL - mmol/L - E - E/t - min - sek - t - dager - g - kg - år - % - %1$d g - %1$d min - %1$d sek - %1$d t - %1$.0f t - %1$d dager - %1$d%% - %1$.1f E - %1$d E - %1$.1f E/t - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d sek (%2$d - %3$d) - %1$d t (%2$d - %3$d) - %1$.0f t (%2$.0f - %3$.0f) - %1$d dager (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d E (%2$d - %3$d) - %1$.1f E (%2$.1f - %3$.1f) - %1$.1f E/t (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Språk Systemstandard Engelsk @@ -417,5 +377,4 @@ BS-verdien som insulinet stanses under. Standardverdien bruker standard målmodell. Brukeren kan angi verdi mellom 60 mg/dL (3,3 mmol/L) og 100 mg/dL (5,5 mmol/L). Verdier under 65/3,6 resulterer i bruk av standardmodellen. - Område for visualisering diff --git a/core/keys/src/main/res/values-ro-rRO/strings.xml b/core/keys/src/main/res/values-ro-rRO/strings.xml index ec59e949e190..9aff4fd1166a 100644 --- a/core/keys/src/main/res/values-ro-rRO/strings.xml +++ b/core/keys/src/main/res/values-ro-rRO/strings.xml @@ -235,48 +235,8 @@ Copil Unități - mg/dL - mmol/L - U - U/h - minute - secunde - h - zile - g - kg - an - % - %1$d g - %1$d min - %1$d sec - %1$d h - %1$.0f h - %1$d zile - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d sec (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d zile (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/o (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Limba Implicit sistem Engleză @@ -417,5 +377,4 @@ Valoarea glicemiei sub care administrarea insulinei este suspendată. Valoarea implicită folosește modelul standard de țintă. Utilizatorul poate seta valoarea între 60mg/dl (3,3mmol/l) și 100 mg/dl (5,5mmol/l). Valorile sub 65/3,6 determină utilizarea modelului standard - Intervalul pentru vizualizare diff --git a/core/keys/src/main/res/values-sk-rSK/strings.xml b/core/keys/src/main/res/values-sk-rSK/strings.xml index 431bca078afb..2d0754d10950 100644 --- a/core/keys/src/main/res/values-sk-rSK/strings.xml +++ b/core/keys/src/main/res/values-sk-rSK/strings.xml @@ -235,48 +235,8 @@ Dieťa Jednotky - mg/dL - mmol/L - U - U/h - min - sek - h - dní - g - kg - r - % - %1$d g - %1$d min - %1$d sek - %1$d h - %1$.0f h - %1$d dní - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d s (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d dní (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Jazyk Východzí systémový Angličtina @@ -417,5 +377,4 @@ Hodnota glykémie, pod ktorú bude podávanie inzulínu zastavené. Východzia hodnota využíva štandardný cieľový model. Užívateľ môže nastaviť hodnoty od 3,3 mmol/l do 5,5 mmol/l. Pri hodnotách pod 3,6 mmol/l se použije východzí model. - Rozsah pre zobrazenie diff --git a/core/keys/src/main/res/values-vi-rVN/strings.xml b/core/keys/src/main/res/values-vi-rVN/strings.xml index 6da1032a9e6c..3b5c99e8dac1 100644 --- a/core/keys/src/main/res/values-vi-rVN/strings.xml +++ b/core/keys/src/main/res/values-vi-rVN/strings.xml @@ -235,48 +235,8 @@ Trẻ em Đơn vị - mg/dl - mmol/L - U - U/h - phút - giây - h - ngày - g - kg - y - % - %1$d g - %1$d phút - %1$d giây - %1$d h - %1$.0f h - %1$d ngày - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d phút (%2$d - %3$d) - %1$d giây (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d ngày (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Ngôn ngữ Mặc định hệ thống Tiếng Anh @@ -417,5 +377,4 @@ Đường huyết dưới mức này sẽ tạm dừng việc tiêm insulin. Mức mặc định sử dụng mô hình mục tiêu chuẩn. Người dùng có thể đặt giá trị trong khoảng 60 mg/dL (3,3 mmol/L) đến 100 mg/dL (5,5 mmol/L). Các giá trị dưới 65 mg/dL (3,6 mmol/L) sẽ sử dụng mô hình mặc định - Phạm vi để hiển thị diff --git a/core/keys/src/main/res/values-zh-rCN/strings.xml b/core/keys/src/main/res/values-zh-rCN/strings.xml index 1f5f551c0e73..12ac05361407 100644 --- a/core/keys/src/main/res/values-zh-rCN/strings.xml +++ b/core/keys/src/main/res/values-zh-rCN/strings.xml @@ -235,48 +235,8 @@ 儿童 单位 - mg/dL - mmol/L - U - U/h - - - 小时 - - g - kg - - % - %1$d g - %1$d 分 - %1$d 秒 - %1$d 小时 - %1$.0f 小时 - %1$d 天 - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d 分(%2$d - %3$d) - %1$d 秒(%2$d - %3$d) - %1$d 小时(%2$d - %3$d) - %1$.0f 小时(%2$.0f - %3$.0f) - %1$d 天 (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) 语言 系统默认 英语 @@ -417,5 +377,4 @@ 血糖值低于此值时,胰岛素暂停使用。默认值使用标准目标模型。用户可以在 60mg/dl (3.3mmol/l) 和 100mg/dl (5.5mmol/l) 之间设置。低于 65/3.6 的值将使用默认模型 - 可视化范围 diff --git a/core/keys/src/main/res/values-zh-rTW/strings.xml b/core/keys/src/main/res/values-zh-rTW/strings.xml index 8b123279e816..7595e9fec31f 100644 --- a/core/keys/src/main/res/values-zh-rTW/strings.xml +++ b/core/keys/src/main/res/values-zh-rTW/strings.xml @@ -236,48 +236,8 @@ 兒童 單位 - mg/dL - mmol/L - U - U/h - 分鐘 - - 小時 - - - 公斤 - - % - %1$d 克 - %1$d 分鐘 - %1$d 秒 - %1$d 小時 - %1$.0f 小時 - %1$d 天 - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d 分鐘 (%2$d - %3$d) - %1$d 秒 (%2$d - %3$d) - %1$d 小時 (%2$d - %3$d) - %1$.0f 小時 (%2$.0f - %3$.0f) - %1$d 天 (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) 語系 系統預設 英文 @@ -418,5 +378,4 @@ 當血糖低於此值時,將暫停胰島素。預設值使用標準目標模型。使用者可將數值設定在 60 mg/dl(3.3 mmol/l)與 100 mg/dl(5.5 mmol/l)之間。若低於 65/3.6,則改用預設模型。 - 視覺化範圍 diff --git a/core/keys/src/main/res/values/strings.xml b/core/keys/src/main/res/values/strings.xml index a8849d5aaee0..61c12ea5090a 100644 --- a/core/keys/src/main/res/values/strings.xml +++ b/core/keys/src/main/res/values/strings.xml @@ -263,50 +263,10 @@ Units - mg/dL - mmol/L - U - U/h - min - sec - h - days - g - kg - y - % - %1$d g - %1$d min - %1$d sec - %1$d h - %1$.0f h - %1$d days - %1$d%% - %1$.1f U - %1$d U - %1$.1f U/h - %1$s mg/dL - %1$.1f - %1$.2f - %1$.3f - %1$d g (%2$d - %3$d) - %1$d min (%2$d - %3$d) - %1$d sec (%2$d - %3$d) - %1$d h (%2$d - %3$d) - %1$.0f h (%2$.0f - %3$.0f) - %1$d days (%2$d - %3$d) - %1$d%% (%2$d - %3$d) - %1$d U (%2$d - %3$d) - %1$.1f U (%2$.1f - %3$.1f) - %1$.1f U/h (%2$.1f - %3$.1f) - %1$s mg/dL (%2$s - %3$s) - %1$.1f (%2$.1f - %3$.1f) - %1$.2f (%2$.2f - %3$.2f) - %1$.3f (%2$.3f - %3$.3f) Language System default English @@ -464,6 +424,5 @@ - Range for visualization diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt index c5e075c2659c..d7aacfe1b9ba 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R -import app.aaps.core.keys.R as KeysR /** * Compact carb time row with inline expand/collapse. @@ -142,7 +141,7 @@ fun CarbTimeRow( onValueChange = { onOffsetChange(it.toInt()) }, valueRange = offsetRange.first.toDouble()..offsetRange.last.toDouble(), step = offsetStep.toDouble(), - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(R.string.units_min) ) // Alarm toggle (disabled when offset <= 0) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index bf611ddbcbd3..5570923f5322 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -48,7 +48,7 @@ import kotlin.math.roundToInt * 0 — 300 ← range, or error message * ``` * - * @param labelResId Resource ID for the display label + * @param labelRef Display label * @param value Current numeric value * @param onValueChange Callback invoked when value changes, receives new value as Double * @param valueRange The range of values the input can represent @@ -70,7 +70,7 @@ import kotlin.math.roundToInt */ @Composable fun NumberInputRow( - labelResId: Int, + labelRef: TextRef?, value: Double, onValueChange: (Double) -> Unit, valueRange: ClosedFloatingPointRange, @@ -90,7 +90,7 @@ fun NumberInputRow( NumberFormat.withDecimals(decimalPlaces) } val focusManager = LocalFocusManager.current - val label = if (labelResId != 0) stringResource(labelResId) else "" + val label = labelRef?.let { stringResource(it) } ?: "" // Track whether the field is focused for editing var isFocused by remember { mutableStateOf(false) } var textFieldValue by remember { @@ -295,4 +295,41 @@ fun NumberInputRow( } } -// --- Previews --- +/** + * Convenience for the many call sites that name their own module's `R.string.x`. + * The [TextRef] form above is the real one; this just wraps the id. + */ +@Composable +fun NumberInputRow( + labelResId: Int, + value: Double, + onValueChange: (Double) -> Unit, + valueRange: ClosedFloatingPointRange, + step: Double, + modifier: Modifier = Modifier, + unitLabel: TextRef? = null, + asDuration: Boolean = false, + valueFormatResId: Int? = null, + formatAsInt: Boolean = false, + valueFormat: NumberFormat? = null, + decimalPlaces: Int = 0, + enabled: Boolean = true, + compact: Boolean = false, + displayValue: String? = null, +) = NumberInputRow( + labelRef = if (labelResId != 0) TextRef.AndroidRes(labelResId) else null, + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + step = step, + modifier = modifier, + unitLabel = unitLabel, + asDuration = asDuration, + valueFormatResId = valueFormatResId, + formatAsInt = formatAsInt, + valueFormat = valueFormat, + decimalPlaces = decimalPlaces, + enabled = enabled, + compact = compact, + displayValue = displayValue, +) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt index 0d3b5c0e7132..175f5d935763 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R -import app.aaps.core.keys.R as KeysR @Preview(showBackground = true) @Composable @@ -41,7 +40,7 @@ internal fun NumberInputRowMinutesPreview() { onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(R.string.units_min) ) } } @@ -56,7 +55,7 @@ internal fun NumberInputRowPercentPreview() { onValueChange = {}, valueRange = 10.0..200.0, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_percent) + unitLabel = TextRef.AndroidRes(R.string.units_percent) ) } } @@ -71,7 +70,7 @@ internal fun NumberInputRowMinutesDirectPreview() { onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(R.string.units_min) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt index a4a755956d8b..27ba75dddea1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.keys.R as KeysR +import app.aaps.core.ui.R @Preview(showBackground = true) @Composable @@ -48,7 +48,7 @@ internal fun SliderWithButtonsIntPreview() { step = 5.0, showValue = true, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(R.string.units_min) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt index ef529c42db34..95b8c19745af 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -9,15 +9,15 @@ import app.aaps.core.keys.interfaces.TextRef * * Every preference screen funnels through this one function, which is the point: when a module later * moves its strings to `commonMain/composeResources`, only this resolver learns about the new - * negative-token form of [TextRef.Res]. The ~18 call sites do not change again. + * negative-token form of [TextRef.AndroidRes]. The ~18 call sites do not change again. * - * On Android today [TextRef.Res.id] is always an ordinary `R.string` id, so this is a direct call + * On Android today [TextRef.AndroidRes.id] is always an ordinary `R.string` id, so this is a direct call * through to the platform. */ @Composable fun stringResource(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text - is TextRef.Res -> + is TextRef.AndroidRes -> if (ref.args.isEmpty()) stringResource(ref.id) else stringResource(ref.id, *ref.args.toTypedArray()) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt index 00dab17087fe..8aa9d065abd5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose import app.aaps.core.keys.UnitType import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.keys.R as KeysR +import app.aaps.core.ui.R /** * Maps a [UnitType] to the text that describes it. @@ -18,26 +18,26 @@ import app.aaps.core.keys.R as KeysR */ fun UnitType.unitLabel(): TextRef? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> TextRef.Res(KeysR.string.units_grams) - UnitType.MIN -> TextRef.Res(KeysR.string.units_min) - UnitType.SEC -> TextRef.Res(KeysR.string.units_sec) - UnitType.HOURS, UnitType.HOURS_DOUBLE -> TextRef.Res(KeysR.string.units_hours) - UnitType.DAYS -> TextRef.Res(KeysR.string.units_days) - UnitType.PERCENT -> TextRef.Res(KeysR.string.units_percent) - UnitType.INSULIN, UnitType.INSULIN_INT -> TextRef.Res(KeysR.string.units_insulin) - UnitType.INSULIN_RATE -> TextRef.Res(KeysR.string.units_insulin_rate) + UnitType.GRAMS -> TextRef.AndroidRes(R.string.units_grams) + UnitType.MIN -> TextRef.AndroidRes(R.string.units_min) + UnitType.SEC -> TextRef.AndroidRes(R.string.units_sec) + UnitType.HOURS, UnitType.HOURS_DOUBLE -> TextRef.AndroidRes(R.string.units_hours) + UnitType.DAYS -> TextRef.AndroidRes(R.string.units_days) + UnitType.PERCENT -> TextRef.AndroidRes(R.string.units_percent) + UnitType.INSULIN, UnitType.INSULIN_INT -> TextRef.AndroidRes(R.string.units_insulin) + UnitType.INSULIN_RATE -> TextRef.AndroidRes(R.string.units_insulin_rate) UnitType.DOUBLE, UnitType.DOUBLE_2, UnitType.DOUBLE_3 -> null // generic doubles carry no unit - UnitType.MGDL -> TextRef.Res(KeysR.string.units_mgdl) + UnitType.MGDL -> TextRef.AndroidRes(R.string.units_mgdl) } /** * A value and its range, already filled in - "5 min (0 - 30)". * - * The arguments are taken here rather than returned as a bare template id, because a [TextRef.Res] + * The arguments are taken here rather than returned as a bare template id, because a [TextRef.AndroidRes] * with no arguments renders the raw `%1$d` template. Taking them makes that mistake impossible. */ fun UnitType.rangeText(value: Any, min: Any, max: Any): TextRef? = - rangeFormatResId()?.let { TextRef.Res(it, listOf(value, min, max)) } + rangeFormatResId()?.let { TextRef.AndroidRes(it, listOf(value, min, max)) } /** * Format template for a single value, e.g. `%1$d min`. @@ -47,38 +47,38 @@ fun UnitType.rangeText(value: Any, min: Any, max: Any): TextRef? = */ fun UnitType.valueFormatResId(): Int? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> KeysR.string.units_format_grams - UnitType.MIN -> KeysR.string.units_format_min - UnitType.SEC -> KeysR.string.units_format_sec - UnitType.HOURS -> KeysR.string.units_format_hours - UnitType.HOURS_DOUBLE -> KeysR.string.units_format_hours_double - UnitType.DAYS -> KeysR.string.units_format_days - UnitType.PERCENT -> KeysR.string.units_format_percent - UnitType.INSULIN -> KeysR.string.units_format_insulin - UnitType.INSULIN_INT -> KeysR.string.units_format_insulin_int - UnitType.INSULIN_RATE -> KeysR.string.units_format_insulin_rate - UnitType.DOUBLE -> KeysR.string.units_format_double - UnitType.DOUBLE_2 -> KeysR.string.units_format_double_2 - UnitType.DOUBLE_3 -> KeysR.string.units_format_double_3 - UnitType.MGDL -> KeysR.string.units_format_mgdl + UnitType.GRAMS -> R.string.units_format_grams + UnitType.MIN -> R.string.units_format_min + UnitType.SEC -> R.string.units_format_sec + UnitType.HOURS -> R.string.units_format_hours + UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double + UnitType.DAYS -> R.string.units_format_days + UnitType.PERCENT -> R.string.units_format_percent + UnitType.INSULIN -> R.string.units_format_insulin + UnitType.INSULIN_INT -> R.string.units_format_insulin_int + UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate + UnitType.DOUBLE -> R.string.units_format_double + UnitType.DOUBLE_2 -> R.string.units_format_double_2 + UnitType.DOUBLE_3 -> R.string.units_format_double_3 + UnitType.MGDL -> R.string.units_format_mgdl } private fun UnitType.rangeFormatResId(): Int? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> KeysR.string.units_format_grams_range - UnitType.MIN -> KeysR.string.units_format_min_range - UnitType.SEC -> KeysR.string.units_format_sec_range - UnitType.HOURS -> KeysR.string.units_format_hours_range - UnitType.HOURS_DOUBLE -> KeysR.string.units_format_hours_double_range - UnitType.DAYS -> KeysR.string.units_format_days_range - UnitType.PERCENT -> KeysR.string.units_format_percent_range - UnitType.INSULIN -> KeysR.string.units_format_insulin_range - UnitType.INSULIN_INT -> KeysR.string.units_format_insulin_int_range - UnitType.INSULIN_RATE -> KeysR.string.units_format_insulin_rate_range - UnitType.DOUBLE -> KeysR.string.units_format_double_range - UnitType.DOUBLE_2 -> KeysR.string.units_format_double_2_range - UnitType.DOUBLE_3 -> KeysR.string.units_format_double_3_range - UnitType.MGDL -> KeysR.string.units_format_mgdl_range + UnitType.GRAMS -> R.string.units_format_grams_range + UnitType.MIN -> R.string.units_format_min_range + UnitType.SEC -> R.string.units_format_sec_range + UnitType.HOURS -> R.string.units_format_hours_range + UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double_range + UnitType.DAYS -> R.string.units_format_days_range + UnitType.PERCENT -> R.string.units_format_percent_range + UnitType.INSULIN -> R.string.units_format_insulin_range + UnitType.INSULIN_INT -> R.string.units_format_insulin_int_range + UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate_range + UnitType.DOUBLE -> R.string.units_format_double_range + UnitType.DOUBLE_2 -> R.string.units_format_double_2_range + UnitType.DOUBLE_3 -> R.string.units_format_double_3_range + UnitType.MGDL -> R.string.units_format_mgdl_range } /** True when this unit should be rendered as a duration ("1 h 30 min") rather than a plain number. */ diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt index ea62d8580011..6036c4b40d07 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import app.aaps.core.keys.StringKey +import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.dialogs.QueryPasswordDialog @@ -58,7 +59,7 @@ fun AdaptiveMasterPasswordPreferenceItem( Preference( title = if (showTitle) { - { Text(stringResource(app.aaps.core.keys.R.string.master_password)) } + { Text(stringResource(StringKey.ProtectionMasterPassword.title)) } } else { { Text(summary) } }, @@ -106,7 +107,7 @@ fun AdaptiveMasterPasswordPreferenceItem( // Set new password dialog if (showSetDialog) { SetPasswordDialog( - title = stringResource(app.aaps.core.keys.R.string.master_password), + title = stringResource(StringKey.ProtectionMasterPassword.title), pinInput = false, onConfirm = { password1, password2 -> when { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index b3827faea6fb..068b189c4450 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -69,7 +69,7 @@ fun AdaptiveStringPreferenceItem( isSecure && value.isEmpty() -> { val notSetResId = if (stringKey.isPin) R.string.pin_not_set else R.string.password_not_set - { Text(stringResource(effectiveSummary ?: TextRef.Res(notSetResId))) } + { Text(stringResource(effectiveSummary ?: TextRef.AndroidRes(notSetResId))) } } value.isNotEmpty() -> { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index d566d3765ea1..3b93b2e38b5c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -63,7 +63,7 @@ fun AdaptiveUnitDoublePreferenceItem( val valueFormat = if (isMgdl) NumberFormat.INTEGER else NumberFormat.DECIMAL_1 // Get unit label from resources - short form for slider - val unitLabel = TextRef.Res(if (isMgdl) UiR.string.mgdl else UiR.string.mmol) + val unitLabel = TextRef.AndroidRes(if (isMgdl) UiR.string.mgdl else UiR.string.mmol) // Get summary if available val summary = stringResourceOrNull(unitKey.summary) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt index fc1b9b59cbf3..fb0ae0d2c98e 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt @@ -14,7 +14,7 @@ import app.aaps.core.ui.R internal fun CollapsibleCardSectionContentPreview() { PreviewTheme { CollapsibleCardSectionContent( - title = TextRef.Res(R.string.configbuilder_insulin), + title = TextRef.AndroidRes(R.string.configbuilder_insulin), expanded = true, onToggle = {} ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt index 214ff3eab990..8f7774c4aa1b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt @@ -29,10 +29,10 @@ data class PreferenceSubScreenDef( ) : PreferenceItem { /** Screen title, in the same form as [PreferenceKey.title]. */ - val title: TextRef = TextRef.Res(titleResId) + val title: TextRef = TextRef.AndroidRes(titleResId) /** Optional summary, in the same form as [PreferenceKey.summary]. */ - val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } /** Titles of the contained items, used to build the summary line in the parent list. */ fun effectiveSummaryItems(): List = diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt index f0153cd24db1..3d2ab69c8f1e 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt @@ -12,7 +12,7 @@ object LocaleHelper { context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE) private fun selectedLanguage(context: Context): String = - // do not use app.aaps.core.keys.R.strings.kay_language = "language" to avoid module dependency + // do not use the key name "language" from :core:keys here, to avoid a module dependency if (defaultSharedPreferences(context).getBoolean("simple_mode", true)) "default" else defaultSharedPreferences(context).getString("language", "default") ?: "default" diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt index 038d5d3faa51..117c5bb4c505 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt @@ -93,11 +93,11 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = elementType.name - override val title: TextRef = TextRef.Res(elementType.labelResId()) + override val title: TextRef = TextRef.AndroidRes(elementType.labelResId()) @Deprecated("use icon") override val icon: ImageVector = elementType.icon() - override val summary: TextRef? = elementType.descriptionResId().takeIf { it != 0 }?.let { TextRef.Res(it) } + override val summary: TextRef? = elementType.descriptionResId().takeIf { it != 0 }?.let { TextRef.AndroidRes(it) } } /** @@ -111,8 +111,8 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = pluginRef.javaClass.simpleName - override val title: TextRef = TextRef.Res(pluginRef.pluginDescription.pluginName) - override val summary: TextRef? = pluginRef.pluginDescription.description.takeIf { it != -1 }?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(pluginRef.pluginDescription.pluginName) + override val summary: TextRef? = pluginRef.pluginDescription.description.takeIf { it != -1 }?.let { TextRef.AndroidRes(it) } override val plugin: PluginBase = pluginRef } diff --git a/core/ui/src/main/res/values-bg-rBG/strings.xml b/core/ui/src/main/res/values-bg-rBG/strings.xml index ab9e0e565703..4f92ad504c07 100644 --- a/core/ui/src/main/res/values-bg-rBG/strings.xml +++ b/core/ui/src/main/res/values-bg-rBG/strings.xml @@ -1088,4 +1088,45 @@ ЧУВСТВ% ПРОМ ЧУВСТВ КРАЧКИ + дни + %1$d дни + %1$d дни (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d гр + %1$d гр (%2$d - %3$d) + %1$d ч + %1$.0f ч + %1$.0f ч (%2$.0f - %3$.0f) + %1$d ч (%2$d - %3$d) + %1$.1f Ед + %1$d Ед + %1$d Ед (%2$d - %3$d) + %1$.1f Ед (%2$.1f - %3$.1f) + %1$.1f Ед/ч + %1$.1f Е/ч (%2$.1f - %3$.1f) + %1$s мг/дл + %1$s мг/дл (%2$s - %3$s) + %1$d мин + %1$d мин (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d сек + %1$d сек (%2$d - %3$d) + гр + ч + Ед + Е/ч + кг + мг/дл + мин + ммол/л + % + сек + г + Диапазон на визуализация diff --git a/core/ui/src/main/res/values-cs-rCZ/strings.xml b/core/ui/src/main/res/values-cs-rCZ/strings.xml index ccc3d6d21dca..9c51af34c770 100644 --- a/core/ui/src/main/res/values-cs-rCZ/strings.xml +++ b/core/ui/src/main/res/values-cs-rCZ/strings.xml @@ -1080,4 +1080,45 @@ SENZ% VSEN KROKY + dní + %1$d dní + %1$d dní (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d s + %1$d s (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + min + mmol/L + % + s + r + Rozsah pro vizualizaci diff --git a/core/ui/src/main/res/values-es-rES/strings.xml b/core/ui/src/main/res/values-es-rES/strings.xml index 6f89d42104d0..de2986083af8 100644 --- a/core/ui/src/main/res/values-es-rES/strings.xml +++ b/core/ui/src/main/res/values-es-rES/strings.xml @@ -1088,4 +1088,45 @@ SENS% VSENS PASOS + días + %1$d días + %1$d días (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d seg + %1$d seg (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + min + mmol/L + % + seg + a + Rango para visualización diff --git a/core/ui/src/main/res/values-fr-rFR/strings.xml b/core/ui/src/main/res/values-fr-rFR/strings.xml index e4adabe34ed3..bf0ab3355827 100644 --- a/core/ui/src/main/res/values-fr-rFR/strings.xml +++ b/core/ui/src/main/res/values-fr-rFR/strings.xml @@ -1089,4 +1089,45 @@ SENS% V_SI PAS + jours + %1$d jours + %1$d jours (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d m + %1$d m (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d s + %1$d s (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + m + mmol/L + % + s + a + Fourchette de visualisation diff --git a/core/ui/src/main/res/values-it-rIT/strings.xml b/core/ui/src/main/res/values-it-rIT/strings.xml index 56df516a2c17..7ec143ad76b1 100644 --- a/core/ui/src/main/res/values-it-rIT/strings.xml +++ b/core/ui/src/main/res/values-it-rIT/strings.xml @@ -1074,4 +1074,45 @@ SENS% VSENS STEPS + giorni + %1$d giorni + %1$d giorni (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d sec + %1$d sec (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + min + mmol/L + % + sec + y + Intervallo di visualizzazione diff --git a/core/ui/src/main/res/values-nb-rNO/strings.xml b/core/ui/src/main/res/values-nb-rNO/strings.xml index 94cfe6efe835..29c732489069 100644 --- a/core/ui/src/main/res/values-nb-rNO/strings.xml +++ b/core/ui/src/main/res/values-nb-rNO/strings.xml @@ -1089,4 +1089,45 @@ SENS% DISF SKRITT + dager + %1$d dager + %1$d dager (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d t + %1$.0f t + %1$.0f t (%2$.0f - %3$.0f) + %1$d t (%2$d - %3$d) + %1$.1f E + %1$d E + %1$d E (%2$d - %3$d) + %1$.1f E (%2$.1f - %3$.1f) + %1$.1f E/t + %1$.1f E/t (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d sek + %1$d sek (%2$d - %3$d) + g + t + E + E/t + kg + mg/dL + min + mmol/L + % + sek + år + Område for visualisering diff --git a/core/ui/src/main/res/values-ro-rRO/strings.xml b/core/ui/src/main/res/values-ro-rRO/strings.xml index 2696c5326ebc..a6a1d7213a8b 100644 --- a/core/ui/src/main/res/values-ro-rRO/strings.xml +++ b/core/ui/src/main/res/values-ro-rRO/strings.xml @@ -1092,4 +1092,45 @@ SENS% VSENS PAȘI + zile + %1$d zile + %1$d zile (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/o (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d sec + %1$d sec (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + minute + mmol/L + % + secunde + an + Intervalul pentru vizualizare diff --git a/core/ui/src/main/res/values-sk-rSK/strings.xml b/core/ui/src/main/res/values-sk-rSK/strings.xml index 6bc8a35945a5..ddec624b9a43 100644 --- a/core/ui/src/main/res/values-sk-rSK/strings.xml +++ b/core/ui/src/main/res/values-sk-rSK/strings.xml @@ -1082,4 +1082,45 @@ SENS% VSENS KROKY + dní + %1$d dní + %1$d dní (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d sek + %1$d s (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + min + mmol/L + % + sek + r + Rozsah pre zobrazenie diff --git a/core/ui/src/main/res/values-vi-rVN/strings.xml b/core/ui/src/main/res/values-vi-rVN/strings.xml index 6e84df1904f0..a7e1e07d1c1d 100644 --- a/core/ui/src/main/res/values-vi-rVN/strings.xml +++ b/core/ui/src/main/res/values-vi-rVN/strings.xml @@ -1086,4 +1086,45 @@ SENS% VSENS STEPS + ngày + %1$d ngày + %1$d ngày (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d phút + %1$d phút (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d giây + %1$d giây (%2$d - %3$d) + g + h + U + U/h + kg + mg/dl + phút + mmol/L + % + giây + y + Phạm vi để hiển thị diff --git a/core/ui/src/main/res/values-zh-rCN/strings.xml b/core/ui/src/main/res/values-zh-rCN/strings.xml index 733fab28f5b5..7c5e52be9185 100644 --- a/core/ui/src/main/res/values-zh-rCN/strings.xml +++ b/core/ui/src/main/res/values-zh-rCN/strings.xml @@ -1085,4 +1085,45 @@ SENS% VSENS STEPS + + %1$d 天 + %1$d 天 (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d 小时 + %1$.0f 小时 + %1$.0f 小时(%2$.0f - %3$.0f) + %1$d 小时(%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d 分 + %1$d 分(%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d 秒 + %1$d 秒(%2$d - %3$d) + g + 小时 + U + U/h + kg + mg/dL + + mmol/L + % + + + 可视化范围 diff --git a/core/ui/src/main/res/values-zh-rTW/strings.xml b/core/ui/src/main/res/values-zh-rTW/strings.xml index f9c22957c473..da3ed77e5a82 100644 --- a/core/ui/src/main/res/values-zh-rTW/strings.xml +++ b/core/ui/src/main/res/values-zh-rTW/strings.xml @@ -1088,4 +1088,45 @@ SENS% VSENS STEPS + + %1$d 天 + %1$d 天 (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d 克 + %1$d g (%2$d - %3$d) + %1$d 小時 + %1$.0f 小時 + %1$.0f 小時 (%2$.0f - %3$.0f) + %1$d 小時 (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d 分鐘 + %1$d 分鐘 (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d 秒 + %1$d 秒 (%2$d - %3$d) + + 小時 + U + U/h + 公斤 + mg/dL + 分鐘 + mmol/L + % + + + 視覺化範圍 diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 2487c4cc22f7..bc9525890e85 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -1159,4 +1159,45 @@ STEPS + days + %1$d days + %1$d days (%2$d - %3$d) + %1$.1f + %1$.2f + %1$.2f (%2$.2f - %3$.2f) + %1$.3f + %1$.3f (%2$.3f - %3$.3f) + %1$.1f (%2$.1f - %3$.1f) + %1$d g + %1$d g (%2$d - %3$d) + %1$d h + %1$.0f h + %1$.0f h (%2$.0f - %3$.0f) + %1$d h (%2$d - %3$d) + %1$.1f U + %1$d U + %1$d U (%2$d - %3$d) + %1$.1f U (%2$.1f - %3$.1f) + %1$.1f U/h + %1$.1f U/h (%2$.1f - %3$.1f) + %1$s mg/dL + %1$s mg/dL (%2$s - %3$s) + %1$d min + %1$d min (%2$d - %3$d) + %1$d%% + %1$d%% (%2$d - %3$d) + %1$d sec + %1$d sec (%2$d - %3$d) + g + h + U + U/h + kg + mg/dL + min + mmol/L + % + sec + y + Range for visualization diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt index c7a991a0e004..42a2242595b5 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt @@ -112,7 +112,7 @@ class MorePreferenceComponentsTest { fun clickableCategoryHeaderRendersTitle() { render { ClickablePreferenceCategoryHeader( - title = TextRef.Res(CoreUiR.string.treatments), + title = TextRef.AndroidRes(CoreUiR.string.treatments), expanded = false, onToggle = {} ) @@ -144,7 +144,7 @@ class MorePreferenceComponentsTest { fun collapsibleCardShowsContentWhenExpanded() { render { CollapsibleCardSectionContent( - title = TextRef.Res(CoreUiR.string.treatments), + title = TextRef.AndroidRes(CoreUiR.string.treatments), expanded = true, onToggle = {}, content = { Text("cardbody") } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt index 9650be2090ea..75dbfb58d03f 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt @@ -382,7 +382,7 @@ class ImportExportPrefsImpl @Inject constructor( } private fun askForMasterPass(activity: FragmentActivity, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) { - passwordCheck.queryPassword(activity, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword, { password -> + passwordCheck.queryPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword, { password -> then(password) }, { rxBus.send(EventShowSnackbar(rh.gs(canceledMsg), EventShowSnackbar.Type.Warning)) @@ -400,8 +400,8 @@ class ImportExportPrefsImpl @Inject constructor( EventShowDialog.Error( title = rh.gs(wrongPwdTitle), message = rh.gs(app.aaps.core.ui.R.string.master_password_missing), - positiveButton = rh.gs(app.aaps.core.keys.R.string.master_password), - onPositive = { passwordCheck.setPassword(activity, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword) } + positiveButton = rh.gs(StringKey.ProtectionMasterPassword.title), + onPositive = { passwordCheck.setPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword) } ) ) exportPasswordDataStore.clearPasswordDataStore(context) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt index 49926c9548c8..198a0c6a31bc 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt @@ -84,7 +84,7 @@ object BiometricCheck { ERROR_USER_CANCELED -> { rxBus.send(EventShowSnackbar(errString.toString(), EventShowSnackbar.Type.Error)) // fallback to master password - passwordCheck.queryPassword(activity, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) + passwordCheck.queryPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) } ERROR_NEGATIVE_BUTTON -> @@ -94,14 +94,14 @@ object BiometricCheck { rxBus.send(EventShowSnackbar(errString.toString(), EventShowSnackbar.Type.Error)) // no pin set // fallback to master password - passwordCheck.queryPassword(activity, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) + passwordCheck.queryPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) } ERROR_NO_SPACE, ERROR_HW_UNAVAILABLE, ERROR_HW_NOT_PRESENT, ERROR_NO_BIOMETRICS -> - passwordCheck.queryPassword(activity, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) + passwordCheck.queryPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword, { ok?.run() }, { cancel?.run() }, { fail?.run() }) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt index 6ca57f0902d0..ad2cb57b2441 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt @@ -19,10 +19,12 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import app.aaps.core.interfaces.protection.ExportPasswordDataStore import app.aaps.core.interfaces.protection.PasswordCheck +import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.crypto.CryptoUtil import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalPreferences @@ -40,7 +42,8 @@ import javax.inject.Inject class PasswordCheckImpl @Inject constructor( private val preferences: Preferences, private val cryptoUtil: CryptoUtil, - private val rxBus: RxBus + private val rxBus: RxBus, + private val rh: ResourceHelper ) : PasswordCheck { @Inject lateinit var exportPasswordDataStore: ExportPasswordDataStore @@ -86,6 +89,16 @@ class PasswordCheckImpl @Inject constructor( cancel: (() -> Unit)?, fail: (() -> Unit)?, pinInput: Boolean + ) = queryPassword(context, TextRef.AndroidRes(labelId), preference, ok, cancel, fail, pinInput) + + override fun queryPassword( + context: Context, + label: TextRef, + preference: StringPreferenceKey, + ok: ((String) -> Unit)?, + cancel: (() -> Unit)?, + fail: (() -> Unit)?, + pinInput: Boolean ) { val password = preferences.get(preference) if (password == "") { @@ -106,7 +119,7 @@ class PasswordCheckImpl @Inject constructor( ) { AapsTheme { QueryPasswordDialog( - title = context.getString(labelId), + title = rh.gs(label), pinInput = pinInput, onConfirm = { enteredPassword -> if (cryptoUtil.checkPassword(enteredPassword, password)) { @@ -147,6 +160,16 @@ class PasswordCheckImpl @Inject constructor( cancel: (() -> Unit)?, clear: (() -> Unit)?, pinInput: Boolean + ) = setPassword(context, TextRef.AndroidRes(labelId), preference, ok, cancel, clear, pinInput) + + override fun setPassword( + context: Context, + label: TextRef, + preference: StringPreferenceKey, + ok: ((String) -> Unit)?, + cancel: (() -> Unit)?, + clear: (() -> Unit)?, + pinInput: Boolean ) { val dialog = Dialog(context) val owner = ComposeDialogOwner() @@ -161,7 +184,7 @@ class PasswordCheckImpl @Inject constructor( ) { AapsTheme { SetPasswordDialog( - title = context.getString(labelId), + title = rh.gs(label), pinInput = pinInput, onConfirm = { enteredPassword, enteredPassword2 -> if (enteredPassword != enteredPassword2) { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt index cc10ce0dfcfb..ed3e2de7f88c 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/keys/ApsIntentKey.kt @@ -30,6 +30,6 @@ enum class ApsIntentKey( ) ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt index 8719ec9ebfe1..35540cd7e2f9 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/actions/ActionEditors.kt @@ -13,6 +13,7 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.Scene import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.NumberInputRow import app.aaps.plugins.automation.R import app.aaps.plugins.automation.actions.Action @@ -35,7 +36,6 @@ import app.aaps.plugins.automation.compose.elements.InputStringEditor import app.aaps.plugins.automation.compose.elements.InputWeekDayEditor import app.aaps.plugins.automation.compose.elements.LabelWithElementRow import app.aaps.plugins.automation.elements.InputPercent -import app.aaps.core.keys.R as KeysR @Composable fun ActionEditor( @@ -149,7 +149,7 @@ fun ActionCarePortalEventEditor(a: ActionCarePortalEvent, tick: Int = 0, onChang onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min) ) InputStringEditor( value = a.note.value, @@ -194,7 +194,7 @@ fun ActionProfileSwitchPercentEditor(a: ActionProfileSwitchPercent, tick: Int = onValueChange = { a.pct.value = it; onChange() }, valueRange = InputPercent.MIN..InputPercent.MAX, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_percent) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent) ) NumberInputRow( labelResId = app.aaps.core.ui.R.string.duration_label, @@ -202,7 +202,7 @@ fun ActionProfileSwitchPercentEditor(a: ActionProfileSwitchPercent, tick: Int = onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min) ) } @@ -226,7 +226,7 @@ fun ActionRunAutotuneEditor( onValueChange = { a.daysBackRef().value = it.toInt(); onChange() }, valueRange = 1.0..30.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_days) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_days) ) InputWeekDayEditor(weekdays = a.daysRef(), onChange = onChange) } @@ -266,7 +266,7 @@ fun ActionStartTempTargetEditor(a: ActionStartTempTarget, tick: Int = 0, onChang valueRange = if (isMmol) Constants.TT_RANGE_MMOL else Constants.TT_RANGE_MGDL, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl) + unitLabel = TextRef.AndroidRes(if (isMmol) CoreUiR.string.units_mmol else CoreUiR.string.units_mgdl) ) NumberInputRow( labelResId = app.aaps.core.ui.R.string.duration_label, @@ -274,7 +274,7 @@ fun ActionStartTempTargetEditor(a: ActionStartTempTarget, tick: Int = 0, onChang onValueChange = { a.duration.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min) ) } diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt index ea439c65fa93..6c5a51c6f7da 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/triggers/TriggerEditors.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.NumberInputRow import app.aaps.plugins.automation.R import app.aaps.plugins.automation.compose.elements.AutomationDropdown @@ -55,7 +56,6 @@ import app.aaps.plugins.automation.triggers.TriggerTempTargetValue import app.aaps.plugins.automation.triggers.TriggerTime import app.aaps.plugins.automation.triggers.TriggerTimeRange import app.aaps.plugins.automation.triggers.TriggerWifiSsid -import app.aaps.core.keys.R as KeysR @Composable fun TriggerEditor( @@ -123,7 +123,7 @@ fun TriggerBgEditor(t: TriggerBg, onChange: () -> Unit, tick: Int = 0) { valueRange = if (isMmol) InputBg.MMOL_MIN..InputBg.MMOL_MAX else InputBg.MGDL_MIN..InputBg.MGDL_MAX, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), + unitLabel = TextRef.AndroidRes(if (isMmol) CoreUiR.string.units_mmol else CoreUiR.string.units_mgdl), compact = true ) } @@ -157,7 +157,7 @@ fun TriggerDeltaEditor(t: TriggerDelta, onChange: () -> Unit, tick: Int = 0) { valueRange = -72.0..72.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), + unitLabel = TextRef.AndroidRes(if (isMmol) CoreUiR.string.units_mmol else CoreUiR.string.units_mgdl), compact = true ) } @@ -177,7 +177,7 @@ fun TriggerCOBEditor(t: TriggerCOB, onChange: () -> Unit, tick: Int = 0) { onValueChange = { t.cob.value = it; onChange() }, valueRange = 0.0..150.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_grams), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_grams), compact = true ) } @@ -198,7 +198,7 @@ fun TriggerIobEditor(t: TriggerIob, onChange: () -> Unit, tick: Int = 0) { valueRange = -20.0..20.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_insulin), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_insulin), compact = true ) } @@ -218,7 +218,7 @@ fun TriggerHeartRateEditor(t: TriggerHeartRate, onChange: () -> Unit, tick: Int onValueChange = { t.heartRate.value = it; onChange() }, valueRange = 30.0..250.0, step = 5.0, - unitLabel = TextRef.Res(R.string.automation_unit_bpm), + unitLabel = TextRef.AndroidRes(R.string.automation_unit_bpm), compact = true ) } @@ -238,7 +238,7 @@ fun TriggerAutosensValueEditor(t: TriggerAutosensValue, onChange: () -> Unit, ti onValueChange = { t.autosens.value = it; onChange() }, valueRange = 0.0..300.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_percent), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent), compact = true ) } @@ -258,7 +258,7 @@ fun TriggerBolusAgoEditor(t: TriggerBolusAgo, onChange: () -> Unit, tick: Int = onValueChange = { t.minutesAgo.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), compact = true ) } @@ -279,7 +279,7 @@ fun TriggerCannulaAgeEditor(t: TriggerCannulaAge, onChange: () -> Unit, tick: In valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), compact = true ) } @@ -300,7 +300,7 @@ fun TriggerInsulinAgeEditor(t: TriggerInsulinAge, onChange: () -> Unit, tick: In valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), compact = true ) } @@ -320,7 +320,7 @@ fun TriggerReservoirLevelEditor(t: TriggerReservoirLevel, onChange: () -> Unit, onValueChange = { t.reservoirLevel.value = it; onChange() }, valueRange = 0.0..800.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_insulin), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_insulin), compact = true ) } @@ -341,7 +341,7 @@ fun TriggerPumpBatteryAgeEditor(t: TriggerPumpBatteryAge, onChange: () -> Unit, valueRange = 0.0..336.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), compact = true ) } @@ -361,7 +361,7 @@ fun TriggerPumpBatteryLevelEditor(t: TriggerPumpBatteryLevel, onChange: () -> Un onValueChange = { t.pumpBatteryLevel.value = it; onChange() }, valueRange = 0.0..100.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_percent), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent), compact = true ) } @@ -382,7 +382,7 @@ fun TriggerSensorAgeEditor(t: TriggerSensorAge, onChange: () -> Unit, tick: Int valueRange = 0.0..720.0, step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), compact = true ) } @@ -407,7 +407,7 @@ fun TriggerPumpLastConnectionEditor(t: TriggerPumpLastConnection, onChange: () - onValueChange = { t.minutesAgo.value = it.toInt(); onChange() }, valueRange = 5.0..(24 * 60.0), step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), compact = true ) } @@ -427,7 +427,7 @@ fun TriggerProfilePercentEditor(t: TriggerProfilePercent, onChange: () -> Unit, onValueChange = { t.pct.value = it; onChange() }, valueRange = InputPercent.MIN..InputPercent.MAX, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_percent), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent), compact = true ) } @@ -455,7 +455,7 @@ fun TriggerTempTargetValueEditor(t: TriggerTempTargetValue, onChange: () -> Unit else Constants.TT_RANGE_MGDL, step = if (isMmol) 0.1 else 1.0, decimalPlaces = if (isMmol) 1 else 0, - unitLabel = TextRef.Res(if (isMmol) KeysR.string.units_mmol else KeysR.string.units_mgdl), + unitLabel = TextRef.AndroidRes(if (isMmol) CoreUiR.string.units_mmol else CoreUiR.string.units_mgdl), compact = true ) } @@ -566,7 +566,7 @@ fun TriggerLocationEditor( onValueChange = { t.distance.value = it; onChange() }, valueRange = 0.0..100000.0, step = 10.0, - unitLabel = TextRef.Res(R.string.automation_unit_meters) + unitLabel = TextRef.AndroidRes(R.string.automation_unit_meters) ) InputLocationModeEditor( value = t.modeSelected.value, diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt index 485ec9a5b13a..78024f12c374 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt @@ -246,13 +246,13 @@ class SWDefinition @Inject constructor( .add(swInfoTextProvider.get().label(R.string.setupwizard_pairing_ws_warning).visibility { !preferences.get(BooleanKey.NsClient3UseWs) }) private val screenPatientName - get() = swScreenProvider.get().with(app.aaps.core.keys.R.string.pref_title_patient_name) + get() = swScreenProvider.get().with(StringKey.GeneralPatientName.title) .skippable(true) - .add(swInfoTextProvider.get().label(app.aaps.core.keys.R.string.pref_summary_patient_name)) + .add(swInfoTextProvider.get().label(StringKey.GeneralPatientName.summary!!)) .add(swEditStringProvider.get().validator(String::isNotEmpty).preference(StringKey.GeneralPatientName)) private val screenMasterPassword - get() = swScreenProvider.get().with(app.aaps.core.keys.R.string.master_password) + get() = swScreenProvider.get().with(StringKey.ProtectionMasterPassword.title) .skippable(false) .add(swEditEncryptedPasswordProvider.get().preference(StringKey.ProtectionMasterPassword).onSetPassword { onSetMasterPassword?.invoke() }) .add(swBreakProvider.get()) diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt index 5257b7fa43ce..b921cc55b115 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt @@ -13,6 +13,8 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventStatus import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.compose.stringResource import app.aaps.plugins.configuration.setupwizard.elements.SWItem import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import javax.inject.Inject @@ -25,7 +27,7 @@ class SWEventListener @Inject constructor( passwordCheck: PasswordCheck ) : SWItem(aapsLogger, rh, rxBus, preferences, passwordCheck) { - private var textLabel = 0 + private var textLabel: TextRef? = null private var status = "" private var visibilityValidator: (() -> Boolean)? = null @@ -36,11 +38,13 @@ class SWEventListener @Inject constructor( return this } - override fun label(label: Int): SWEventListener { + override fun label(label: TextRef): SWEventListener { textLabel = label return this } + override fun label(label: Int): SWEventListener = label(TextRef.AndroidRes(label)) + fun initialStatus(status: String): SWEventListener { this.status = status return this @@ -65,7 +69,7 @@ class SWEventListener @Inject constructor( } onDispose { disposable.dispose() } } - val labelText = if (textLabel != 0) stringResource(textLabel) else "" + val labelText = textLabel?.let { stringResource(it) } ?: "" Text(text = "$labelText ${statusState.value}".trim()) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWScreen.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWScreen.kt index dcece5751b46..2b58ba889420 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWScreen.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWScreen.kt @@ -1,14 +1,15 @@ package app.aaps.plugins.configuration.setupwizard import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.compose.stringResource import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.plugins.configuration.setupwizard.elements.SWItem import javax.inject.Inject class SWScreen @Inject constructor(private val rh: ResourceHelper) { - var header: Int = 0 + var header: TextRef? = null private set var items: MutableList = ArrayList() @@ -16,19 +17,18 @@ class SWScreen @Inject constructor(private val rh: ResourceHelper) { var visibility: (() -> Boolean)? = null var skippable = false - fun with(header: Int): SWScreen { + fun with(header: TextRef): SWScreen { this.header = header return this } - fun getHeader(): String { - return rh.gs(header) - } + /** Convenience for the many call sites that pass their own module's `R.string.x`. */ + fun with(header: Int): SWScreen = with(TextRef.AndroidRes(header)) + + fun getHeader(): String = header?.let { rh.gs(it) } ?: "" @Composable - fun getHeaderCompose(): String { - return stringResource(header) - } + fun getHeaderCompose(): String = header?.let { stringResource(it) } ?: "" fun skippable(skippable: Boolean): SWScreen { this.skippable = skippable diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt index 5814b3587214..c17e00cd7b6f 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditIntNumber.kt @@ -29,7 +29,7 @@ class SWEditIntNumber @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHe override fun Compose() { AdaptiveIntPreferenceItem( intKey = preference as IntPreferenceKey, - title = label?.let { TextRef.Res(it) } + title = label ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt index 1b73ad1588df..90682dbcc4ab 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumber.kt @@ -29,7 +29,7 @@ class SWEditNumber @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelpe override fun Compose() { AdaptiveDoublePreferenceItem( doubleKey = preference as DoublePreferenceKey, - title = label?.let { TextRef.Res(it) } + title = label ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt index 1b5bbca10b44..6250470d4002 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditNumberWithUnits.kt @@ -31,7 +31,7 @@ class SWEditNumberWithUnits @Inject constructor(aapsLogger: AAPSLogger, rh: Reso override fun Compose() { AdaptiveUnitDoublePreferenceItem( unitKey = preference as UnitDoublePreferenceKey, - title = label?.let { TextRef.Res(it) } + title = label ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt index fe175792c33d..ee04262de33b 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditString.kt @@ -35,7 +35,7 @@ class SWEditString @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelpe override fun Compose() { InlineStringPreferenceItem( stringKey = preference as StringPreferenceKey, - title = label?.let { TextRef.Res(it) } + title = label ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt index ff2b53ba7612..71d7f628a749 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWEditUrl.kt @@ -29,7 +29,7 @@ class SWEditUrl @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, override fun Compose() { InlineStringPreferenceItem( stringKey = preference as StringPreferenceKey, - title = label?.let { TextRef.Res(it) } + title = label ) } } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWHtmlLink.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWHtmlLink.kt index b6c602898e87..0bb81dc14cdb 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWHtmlLink.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWHtmlLink.kt @@ -13,6 +13,8 @@ import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.compose.stringResource import javax.inject.Inject class SWHtmlLink @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, rxBus: RxBus, preferences: Preferences, passwordCheck: PasswordCheck) : SWItem(aapsLogger, rh, rxBus, preferences, passwordCheck) { @@ -20,11 +22,13 @@ class SWHtmlLink @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, private var textLabel: String? = null private var visibilityValidator: (() -> Boolean)? = null - override fun label(@StringRes label: Int): SWHtmlLink { + override fun label(label: TextRef): SWHtmlLink { this.label = label return this } + override fun label(@StringRes label: Int): SWHtmlLink = label(TextRef.AndroidRes(label)) + fun label(newLabel: String): SWHtmlLink { textLabel = newLabel return this diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWInfoText.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWInfoText.kt index 7bc75904b3c8..fb6ee71e8d9e 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWInfoText.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWInfoText.kt @@ -8,6 +8,8 @@ import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.compose.stringResource import javax.inject.Inject class SWInfoText @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, rxBus: RxBus, preferences: Preferences, passwordCheck: PasswordCheck) : SWItem(aapsLogger, rh, rxBus, preferences, passwordCheck) { @@ -15,11 +17,13 @@ class SWInfoText @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelper, private var textLabel: String? = null private var visibilityValidator: (() -> Boolean)? = null - override fun label(label: Int): SWInfoText { + override fun label(label: TextRef): SWInfoText { this.label = label return this } + override fun label(label: Int): SWInfoText = label(TextRef.AndroidRes(label)) + fun label(newLabel: String): SWInfoText { textLabel = newLabel return this diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt index 3e8e17bba035..20c9c8ec2267 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt @@ -11,6 +11,7 @@ import app.aaps.core.keys.interfaces.PreferenceKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringNonPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey +import app.aaps.core.keys.interfaces.TextRef import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.disposables.Disposable import java.util.concurrent.TimeUnit @@ -26,15 +27,18 @@ open class SWItem @Inject constructor( private var scheduledEventPost: Disposable? = null - var label: Int? = null + var label: TextRef? = null var comment: Int? = null var preference: PreferenceKey? = null - open fun label(@StringRes label: Int): SWItem { + open fun label(label: TextRef): SWItem { this.label = label return this } + /** Convenience for the many call sites that pass their own module's `R.string.x`. */ + open fun label(@StringRes label: Int): SWItem = label(TextRef.AndroidRes(label)) + fun comment(@StringRes comment: Int): SWItem { this.comment = comment return this diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt index 98e3ff2ecebd..fac4f76c910d 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWRadioButton.kt @@ -49,7 +49,7 @@ class SWRadioButton @Inject constructor(aapsLogger: AAPSLogger, rh: ResourceHelp } InlineStringListPreferenceItem( stringKey = key, - title = label?.let { TextRef.Res(it) }, + title = label, entries = entries ) } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt index 3249e37c9317..f4a2677fabe7 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt @@ -85,7 +85,7 @@ class Objective0 @Inject constructor( showMessage(rh.gs(app.aaps.core.ui.R.string.master_password_not_set)) } else { passwordCheck.queryPassword( - context, app.aaps.core.keys.R.string.master_password, StringKey.ProtectionMasterPassword, + context, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword, ok = { task.answered = true callback.run() diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt index feaab41b50b1..83f153651139 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraBooleanKey.kt @@ -31,6 +31,6 @@ enum class InstaraBooleanKey( ) ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt index 520050608e5d..3d91e4e6904e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminBooleanKey.kt @@ -23,5 +23,5 @@ enum class GarminBooleanKey( LocalHttpServer("communication_http", false, titleResId = R.string.garmin_local_http_server, defaultedBySM = true, hideParentScreenIfHidden = true), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt index 1f2e32464a36..0d8bb4d82e82 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminIntKey.kt @@ -26,5 +26,5 @@ enum class GarminIntKey( LocalHttpPort("communication_http_port", 28891, 1001, 65535, dependency = GarminBooleanKey.LocalHttpServer, titleResId = R.string.garmin_local_http_server_port, defaultedBySM = true, hideParentScreenIfHidden = true), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt index 9de5d3a00dca..02443da116c7 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/keys/GarminStringKey.kt @@ -26,5 +26,5 @@ enum class GarminStringKey( RequestKey(key = "garmin_aaps_key", defaultValue = "", titleResId = R.string.garmin_request_key), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt index 80492f218e7b..4e5c740618dd 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/keys/SmsIntentKey.kt @@ -29,6 +29,6 @@ enum class SmsIntentKey( ) ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt index 05f69bb00cee..5a2331a3f566 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/keys/TidepoolBooleanKey.kt @@ -24,6 +24,6 @@ enum class TidepoolBooleanKey( UseTestServers("tidepool_dev_servers", false, titleResId = R.string.title_tidepool_dev_servers, summaryResId = R.string.summary_tidepool_dev_servers), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt index 0370aee68517..f9026f9a242b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/keys/XdripIntentKey.kt @@ -29,6 +29,6 @@ enum class XdripIntentKey( ) ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt index 7ea74aabe34a..aa128079f3ec 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboBooleanKey.kt @@ -25,5 +25,5 @@ enum class ComboBooleanKey( VerboseLogging("combov2_verbose_logging", false, titleResId = R.string.combov2_verbose_logging), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt index 032d2553ae6a..ac8fdd2b2530 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/keys/ComboIntKey.kt @@ -26,5 +26,5 @@ enum class ComboIntKey( DiscoveryDuration("combov2_bt_discovery_duration", defaultValue = 300, titleResId = R.string.combov2_discovery_duration, min = 30, max = 300), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt index bfe7abf819a7..07a87487719e 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaUserOptionsScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.pump.dana.R @@ -182,7 +183,7 @@ internal fun DanaUserOptionsContent( valueRange = 5.0..240.0, step = 5.0, formatAsInt = true, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_sec), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_sec), modifier = itemModifier ) @@ -194,7 +195,7 @@ internal fun DanaUserOptionsContent( valueRange = state.minBacklight.toDouble()..60.0, step = 1.0, formatAsInt = true, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_sec), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_sec), modifier = itemModifier ) @@ -215,7 +216,7 @@ internal fun DanaUserOptionsContent( valueRange = 0.0..24.0, step = 1.0, formatAsInt = true, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), modifier = itemModifier ) diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt index a9003af8e809..7f12d81211f4 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaBooleanKey.kt @@ -26,6 +26,6 @@ enum class DanaBooleanKey( LogInsulinChange("rs_loginsulinchange", true, titleResId = R.string.rs_loginsulinchange_title, summaryResId = R.string.rs_loginsulinchange_summary), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt index 849e945dbfe7..0f77ef899fc6 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntKey.kt @@ -39,6 +39,6 @@ enum class DanaIntKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt index 33a479b6a2b0..83a5e7f5eff3 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/keys/DanaIntentKey.kt @@ -21,5 +21,5 @@ enum class DanaIntentKey( BtSelector(key = "dana_rs_bt_selector", titleResId = R.string.selectedpump) ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt index cd78ed586aea..c9cd57215cfa 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnBooleanKey.kt @@ -28,6 +28,6 @@ enum class DiaconnBooleanKey( SendLogsToCloud("diaconn_g8_cloudsend", true, titleResId = R.string.diaconn_g8_cloudsend_title, summaryResId = R.string.diaconn_g8_cloudsend_summary), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt index 3e54725e3fc9..84e384c76d9e 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntKey.kt @@ -44,6 +44,6 @@ enum class DiaconnIntKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt index 64235b9b72df..7c0a10f2b395 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/keys/DiaconnIntentKey.kt @@ -21,5 +21,5 @@ enum class DiaconnIntentKey( BtSelector(key = "diaconn_bt_selector", titleResId = R.string.selectedpump) ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index ee2ea58c9e20..22c3a63ade43 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -36,7 +36,7 @@ import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withEntries -import app.aaps.core.keys.R as KeysR +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.icons.IcPluginEopatch import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.pump.eopatch.alarm.IAlarmManager @@ -576,10 +576,10 @@ class EopatchPumpPlugin @Inject constructor( // The labels used to be built as "$it U" and "$it hr", which no translator could reach. // The unit format templates already exist and are translated, so use those. EopatchIntKey.LowReservoirReminder.withEntries( - (10..50 step 5).associateWith { TextRef.Res(KeysR.string.units_format_insulin_int, listOf(it)) } + (10..50 step 5).associateWith { TextRef.AndroidRes(CoreUiR.string.units_format_insulin_int, listOf(it)) } ), EopatchIntKey.ExpirationReminder.withEntries( - (1..24).associateWith { TextRef.Res(KeysR.string.units_format_hours, listOf(it)) } + (1..24).associateWith { TextRef.AndroidRes(CoreUiR.string.units_format_hours, listOf(it)) } ), EopatchBooleanKey.BuzzerReminder ), diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt index 7ff7992d359b..a59553961def 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchBooleanKey.kt @@ -23,5 +23,5 @@ enum class EopatchBooleanKey( BuzzerReminder("eopatch_patch_buzzer_reminders", false, titleResId = R.string.patch_buzzer_reminders), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt index e76d91366bbc..3af83bb0c426 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/keys/EopatchIntKey.kt @@ -30,6 +30,6 @@ enum class EopatchIntKey( ExpirationReminder("eopatch_expiration_reminders", 4, titleResId = R.string.patch_expiration_reminders, preferenceType = PreferenceType.LIST), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt index 681428c5e144..bb69936c4aad 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilBooleanPreferenceKey.kt @@ -24,5 +24,5 @@ enum class EquilBooleanPreferenceKey( EquilAlarmInsulin("key_equil_alarm_insulin", true, titleResId = R.string.equil_settings_alarm_insulin), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt index e3b21fde9592..8c865238848a 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/keys/EquilIntPreferenceKey.kt @@ -42,6 +42,6 @@ enum class EquilIntPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } } diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt index 605f56883fe3..32bce13dadd0 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightBooleanKey.kt @@ -32,6 +32,6 @@ enum class InsightBooleanKey( DisableVibrationAuto("insight_disable_vibration_auto", false, titleResId = R.string.disable_vibration_auto, summaryResId = R.string.disable_vibration_auto_summary), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt index 187918f3aa6b..b53c38aa4e73 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/keys/InsightIntKey.kt @@ -28,5 +28,5 @@ enum class InsightIntKey( DisconnectDelay("insight_disconnect_delay", 5, titleResId = R.string.disconnect_delay), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt index cc946893f718..942ee6cf0035 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicBooleanPreferenceKey.kt @@ -29,6 +29,6 @@ enum class MedtronicBooleanPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt index 697fc79220c6..7d3e2b6950a9 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicIntPreferenceKey.kt @@ -56,7 +56,7 @@ enum class MedtronicIntPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt index 1592eadfef55..29ade03f851c 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/keys/MedtronicStringPreferenceKey.kt @@ -79,7 +79,7 @@ enum class MedtronicStringPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index c9dede3f111c..0e482c3838ea 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -460,19 +460,19 @@ class MedtrumPlugin @Inject constructor( // For NANO and 300U pumps, only Beep and Silent options are available return when (medtrumPump.pumpType()) { PumpType.MEDTRUM_NANO, PumpType.MEDTRUM_300U -> mapOf( - "6" to TextRef.Res(R.string.alarm_setting_beep), - "7" to TextRef.Res(R.string.alarm_setting_silent) + "6" to TextRef.AndroidRes(R.string.alarm_setting_beep), + "7" to TextRef.AndroidRes(R.string.alarm_setting_silent) ) else -> mapOf( - "0" to TextRef.Res(R.string.alarm_setting_light_vibrate_beep), - "1" to TextRef.Res(R.string.alarm_setting_light_vibrate), - "2" to TextRef.Res(R.string.alarm_setting_light_beep), - "3" to TextRef.Res(R.string.alarm_setting_light), - "4" to TextRef.Res(R.string.alarm_setting_vibrate_beep), - "5" to TextRef.Res(R.string.alarm_setting_vibrate), - "6" to TextRef.Res(R.string.alarm_setting_beep), - "7" to TextRef.Res(R.string.alarm_setting_silent) + "0" to TextRef.AndroidRes(R.string.alarm_setting_light_vibrate_beep), + "1" to TextRef.AndroidRes(R.string.alarm_setting_light_vibrate), + "2" to TextRef.AndroidRes(R.string.alarm_setting_light_beep), + "3" to TextRef.AndroidRes(R.string.alarm_setting_light), + "4" to TextRef.AndroidRes(R.string.alarm_setting_vibrate_beep), + "5" to TextRef.AndroidRes(R.string.alarm_setting_vibrate), + "6" to TextRef.AndroidRes(R.string.alarm_setting_beep), + "7" to TextRef.AndroidRes(R.string.alarm_setting_silent) ) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt index 6ced33005ca3..f19d5a53d2cd 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumBooleanKey.kt @@ -41,6 +41,6 @@ enum class MedtrumBooleanKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt index 3f95724bd29b..c69c6ad06899 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumIntKey.kt @@ -51,6 +51,6 @@ enum class MedtrumIntKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt index d609687d7581..08f42a9136ba 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/keys/MedtrumStringKey.kt @@ -48,7 +48,7 @@ enum class MedtrumStringKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt index 55c8157d213f..d3a1a4a31f67 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/DashBooleanPreferenceKey.kt @@ -24,5 +24,5 @@ enum class DashBooleanPreferenceKey( UseBonding("AAPS.Omnipod.Dash.use_bonding", false, titleResId = R.string.omnipod_dash_use_bonding), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt index 3a374560e941..514e6c9fe3b5 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodBooleanPreferenceKey.kt @@ -43,6 +43,6 @@ enum class OmnipodBooleanPreferenceKey( AutomaticallyAcknowledgeAlerts("AAPS.Omnipod.automatically_acknowledge_alerts_enabled", false, titleResId = R.string.omnipod_common_preferences_automatically_silence_alerts); override val preferenceType: PreferenceType = PreferenceType.SWITCH - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt index eb00024834db..510033c319ae 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/keys/OmnipodIntPreferenceKey.kt @@ -43,7 +43,7 @@ enum class OmnipodIntPreferenceKey( ); override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt index a3c4b9941495..451776978523 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/keys/ErosBooleanPreferenceKey.kt @@ -29,5 +29,5 @@ enum class ErosBooleanPreferenceKey( TimeChangeEnabled("AAPS.Omnipod.time_change_enabled", true, titleResId = CommonR.string.omnipod_common_preferences_time_change_enabled), ; - override val title: TextRef = TextRef.Res(titleResId) + override val title: TextRef = TextRef.AndroidRes(titleResId) } diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt index 34622a3fad8e..afa0c5f9f441 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkStringPreferenceKey.kt @@ -37,7 +37,7 @@ enum class RileyLinkStringPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.Res(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt index e0def9e949a0..a98b3e7357b8 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileylinkBooleanPreferenceKey.kt @@ -35,6 +35,6 @@ enum class RileylinkBooleanPreferenceKey( ), ; - override val title: TextRef = TextRef.Res(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.Res(it) } + override val title: TextRef = TextRef.AndroidRes(titleResId) + override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt index 6dfdcedc6563..88518c562c9c 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt @@ -330,7 +330,7 @@ internal fun CarbsDialogContent( valueRange = (-uiState.cobLimit).toDouble()..uiState.maxCarbs.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(CoreUiR.string.shortgramm) + unitLabel = TextRef.AndroidRes(CoreUiR.string.shortgramm) ) // Removing carbs (negative): show the COB-bounded limit so the user understands why it can't go lower. if (uiState.carbs < 0) { @@ -356,7 +356,7 @@ internal fun CarbsDialogContent( valueRange = 0.0..uiState.maxCarbsDurationHours.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(InterfacesR.string.shorthour), + unitLabel = TextRef.AndroidRes(InterfacesR.string.shorthour), modifier = itemModifier ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt index 24b592aadba2..ad60ddb806f0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/careDialog/CareDialogScreen.kt @@ -61,7 +61,6 @@ import app.aaps.core.ui.compose.siteRotation.SiteLocationSummary import app.aaps.ui.R import app.aaps.ui.compose.EventDatePicker import app.aaps.ui.compose.EventTimePicker -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR @Composable @@ -375,7 +374,7 @@ private fun DurationSection( onValueChange = onDurationChange, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), modifier = modifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt index 8507e82feeb3..0826f48256b8 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt @@ -48,7 +48,6 @@ import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.ui.compose.navigation.labelResId -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR @Composable @@ -197,7 +196,7 @@ internal fun ExtendedBolusDialogContent( valueRange = uiState.minInsulin..uiState.maxInsulin, step = uiState.extendedStep, valueFormat = NumberFormat.DECIMAL_2, - unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.AndroidRes(CoreUiR.string.insulin_unit_shortname), modifier = itemModifier ) @@ -208,7 +207,7 @@ internal fun ExtendedBolusDialogContent( valueRange = uiState.extendedDurationStep..uiState.extendedMaxDuration, step = uiState.extendedDurationStep, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt index 6e142b864b35..63f48db5490c 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt @@ -383,7 +383,7 @@ internal fun FillDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.AndroidRes(CoreUiR.string.insulin_unit_shortname), enabled = uiState.showBolus ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt index 9dda87d5227f..ad0e0f72974d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt @@ -71,7 +71,6 @@ import app.aaps.ui.compose.overview.chips.CobUiState import app.aaps.ui.compose.overview.chips.IobUiState import app.aaps.ui.compose.overview.graphs.BgInfoUiState import kotlinx.coroutines.flow.StateFlow -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR @Composable @@ -357,7 +356,7 @@ internal fun InsulinDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname) + unitLabel = TextRef.AndroidRes(CoreUiR.string.insulin_unit_shortname) ) InsulinQuickAddButtons( increment1 = uiState.insulinButtonIncrement1, @@ -404,7 +403,7 @@ internal fun InsulinDialogContent( onValueChange = onTimeOffsetChange, valueRange = -12.0 * 60..12.0 * 60, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_min) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min) ) DateTimeSection( dateString = dateString, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt index d64e54bf1714..69afbdfd7360 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt @@ -73,7 +73,6 @@ import app.aaps.core.ui.compose.insulin.ConcentrationDropdown import app.aaps.core.ui.compose.masterEditingEnabled import app.aaps.ui.R import app.aaps.ui.compose.components.ManagementCarousel -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR /** @@ -365,7 +364,7 @@ fun InsulinManagementScreen( onValueChange = { viewModel.updateEditorPeak(it.toInt()) }, valueRange = viewModel.peakRange(), step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), enabled = editorEnabled, modifier = Modifier.fillMaxWidth() ) @@ -388,7 +387,7 @@ fun InsulinManagementScreen( valueRange = viewModel.diaRange(), step = 0.1, decimalPlaces = 1, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours), enabled = editorEnabled, modifier = Modifier.fillMaxWidth() ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt index a1aadab6f21e..beb0bee68875 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt @@ -7,12 +7,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.aaps.core.keys.StringKey import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.ui.compose.dialogs.OkDialog import app.aaps.core.ui.compose.dialogs.QueryAnyPasswordDialog import app.aaps.ui.compose.maintenance.MaintenanceViewModel.ExportState -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR /** @@ -209,7 +210,7 @@ fun MaintenanceDialogs( is ExportState.AskPassword -> { val askState = exportState as ExportState.AskPassword QueryAnyPasswordDialog( - title = stringResource(KeysR.string.master_password), + title = stringResource(StringKey.ProtectionMasterPassword.title), passwordExplanation = stringResource(CoreUiR.string.password_preferences_encrypt_prompt), errorMessage = if (askState.wrongPassword) stringResource(CoreUiR.string.wrongpassword) else null, onConfirm = { password -> maintenanceViewModel.onExportPasswordEntered(password) }, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt index 660d7dc4ef3d..1cc1ea362e49 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt @@ -59,6 +59,7 @@ import app.aaps.core.graph.profile.buildTargetRows import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.profile.ProfileSealed +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea @@ -489,7 +490,7 @@ fun DefaultProfileContent( onValueChange = { onAgeChange(it.toInt()) }, valueRange = 1.0..99.0, step = 1.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_years) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_years) ) if (showTdd) NumberInputRow( labelResId = app.aaps.core.ui.R.string.tdd_total, @@ -497,7 +498,7 @@ fun DefaultProfileContent( onValueChange = onTddChange, valueRange = 0.0..200.0, step = 1.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_insulin) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_insulin) ) if (showWeight) NumberInputRow( labelResId = R.string.weight_label, @@ -505,7 +506,7 @@ fun DefaultProfileContent( onValueChange = onWeightChange, valueRange = 0.0..150.0, step = 1.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_kg) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_kg) ) if (showPct) NumberInputRow( labelResId = R.string.basal_pct_from_tdd_label, @@ -513,7 +514,7 @@ fun DefaultProfileContent( onValueChange = onPctChange, valueRange = 32.0..37.0, step = 1.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent) ) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt index 2b86e2015011..38da99ec5b97 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileActivationScreen.kt @@ -53,6 +53,7 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.DateTimeSection import app.aaps.core.ui.compose.EventTimeRow @@ -273,7 +274,7 @@ fun ProfileActivationScreen( onValueChange = { percentage = it }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent), modifier = itemModifier ) @@ -284,7 +285,7 @@ fun ProfileActivationScreen( onValueChange = { duration = it }, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), modifier = itemModifier ) @@ -331,7 +332,7 @@ fun ProfileActivationScreen( onValueChange = { timeshift = it }, valueRange = Constants.CPP_TIMESHIFT_RANGE, step = 1.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_hours) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_hours) ) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt index 7d4ab19f14b1..e096e5408df6 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt @@ -47,6 +47,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.configuration.Constants import app.aaps.core.interfaces.navigation.ElementCategory import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.TonalIcon @@ -431,7 +432,7 @@ private fun ProfilePresetDialog( onValueChange = { percentage = it.toInt() }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_percent) ) NumberInputRow( @@ -440,7 +441,7 @@ private fun ProfilePresetDialog( onValueChange = { durationMinutes = it.toInt() }, valueRange = Constants.ACTION_DURATION, step = 10.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_min) + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min) ) if (durationMinutes == 0) { Text( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt index 41f28fa0894d..05d687a92b31 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardEditor.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import app.aaps.core.keys.IntKey import app.aaps.core.data.configuration.Constants import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.TextRef @@ -37,7 +38,6 @@ import app.aaps.core.ui.compose.icons.IcCarbs import app.aaps.core.ui.compose.icons.IcQuickwizard import app.aaps.ui.R import app.aaps.ui.compose.quickWizard.viewmodels.TrendOption -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreR /** @@ -199,7 +199,7 @@ fun QuickWizardEditor( valueRange = 0.0..maxInsulin, step = 0.05, decimalPlaces = 2, - unitLabel = TextRef.Res(CoreR.string.insulin_unit_shortname), + unitLabel = TextRef.AndroidRes(CoreR.string.insulin_unit_shortname), modifier = Modifier.fillMaxWidth() ) } @@ -212,7 +212,7 @@ fun QuickWizardEditor( onValueChange = { onCarbsChange(it.toInt()) }, valueRange = 0.0..maxCarbs, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_grams), + unitLabel = TextRef.AndroidRes(CoreR.string.units_grams), modifier = Modifier.fillMaxWidth() ) } @@ -225,7 +225,7 @@ fun QuickWizardEditor( onValueChange = { onCarbTimeChange(it.toInt()) }, valueRange = -60.0..60.0, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreR.string.units_min), modifier = Modifier.fillMaxWidth() ) } @@ -341,12 +341,12 @@ fun QuickWizardEditor( // Percentage NumberInputRow( - labelResId = KeysR.string.pref_title_bolus_percentage, + labelRef = IntKey.OverviewBolusPercentage.title, value = percentage.toDouble(), onValueChange = { onPercentageChange(it.toInt()) }, valueRange = Constants.WIZARD_PERCENTAGE_RANGE, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_percent), + unitLabel = TextRef.AndroidRes(CoreR.string.units_percent), modifier = Modifier.fillMaxWidth() ) @@ -399,7 +399,7 @@ fun QuickWizardEditor( onValueChange = { onTimeChange(it.toInt()) }, valueRange = (-7 * 24 * 60).toDouble()..(12 * 60).toDouble(), step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreR.string.units_min), modifier = Modifier.fillMaxWidth() ) @@ -410,7 +410,7 @@ fun QuickWizardEditor( onValueChange = { onDurationChange(it.toInt()) }, valueRange = 0.0..10.0, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_hours), + unitLabel = TextRef.AndroidRes(CoreR.string.units_hours), modifier = Modifier.fillMaxWidth() ) @@ -421,7 +421,7 @@ fun QuickWizardEditor( onValueChange = { onCarbs2Change(it.toInt()) }, valueRange = 0.0..maxCarbs, step = 1.0, - unitLabel = TextRef.Res(KeysR.string.units_grams), + unitLabel = TextRef.AndroidRes(CoreR.string.units_grams), modifier = Modifier.fillMaxWidth() ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt index bd0325f25191..80bd5f5f634a 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/ActionEditors.kt @@ -145,7 +145,7 @@ internal fun ProfileSwitchEditor( onValueChange = { onUpdate(action.copy(percentage = it.toInt())) }, valueRange = Constants.CPP_PERCENTAGE_RANGE, step = 5.0, - unitLabel = TextRef.Res(app.aaps.core.keys.R.string.units_percent) + unitLabel = TextRef.AndroidRes(R.string.units_percent) ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt index 6a95791adfc2..d0dabc094e98 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt @@ -47,7 +47,6 @@ import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog import app.aaps.core.ui.compose.navigation.labelResId -import app.aaps.core.keys.R as KeysR import app.aaps.core.ui.R as CoreUiR @Composable @@ -205,7 +204,7 @@ internal fun TempBasalDialogContent( valueRange = 0.0..uiState.maxTempAbsolute, step = uiState.tempAbsoluteStep, valueFormat = NumberFormat.DECIMAL_2, - unitLabel = TextRef.Res(CoreUiR.string.profile_ins_units_per_hour), + unitLabel = TextRef.AndroidRes(CoreUiR.string.profile_ins_units_per_hour), modifier = itemModifier ) } @@ -218,7 +217,7 @@ internal fun TempBasalDialogContent( valueRange = uiState.tempDurationStep..uiState.tempMaxDuration, step = uiState.tempDurationStep, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(CoreUiR.string.units_min), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt index 1bdaed432bc0..22b6f3f651b7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetEditor.kt @@ -31,7 +31,6 @@ import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalDateUtil import app.aaps.core.ui.compose.NumberInputRow -import app.aaps.core.keys.R as KeysR /** * Editor component for temp target presets with inline activation fields. @@ -126,7 +125,7 @@ fun TempTargetEditor( onValueChange = { onDurationChange((it * 60000L).toLong()) }, valueRange = Constants.ACTION_DURATION, step = 5.0, - unitLabel = TextRef.Res(KeysR.string.units_min), + unitLabel = TextRef.AndroidRes(R.string.units_min), modifier = Modifier.fillMaxWidth() ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt index 17ccffe57488..5b6b142a6214 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt @@ -230,7 +230,7 @@ internal fun TreatmentDialogContent( valueRange = 0.0..uiState.maxInsulin, step = uiState.bolusStep, valueFormat = bolusFormat, - unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.AndroidRes(CoreUiR.string.insulin_unit_shortname), modifier = itemModifier ) @@ -241,7 +241,7 @@ internal fun TreatmentDialogContent( valueRange = 0.0..uiState.maxCarbs.toDouble(), step = 1.0, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.Res(CoreUiR.string.shortgramm), + unitLabel = TextRef.AndroidRes(CoreUiR.string.shortgramm), modifier = itemModifier ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ExtendedBolusScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ExtendedBolusScreen.kt index 3480a618e97d..c719dc473509 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ExtendedBolusScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ExtendedBolusScreen.kt @@ -41,6 +41,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.objects.extensions.iobCalc import app.aaps.core.objects.extensions.isInProgress +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil @@ -215,7 +216,7 @@ private fun ExtendedBolusItem( append(" ") // Duration append(T.msecs(extendedBolus.duration).mins().toInt().toString()) - append(stringResource(app.aaps.core.keys.R.string.units_min)) + append(stringResource(CoreUiR.string.units_min)) }, modifier = Modifier.padding(start = 4.dp), fontSize = 14.sp, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ProfileSwitchScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ProfileSwitchScreen.kt index 80a5d21a501e..c2076fb5f3b7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ProfileSwitchScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/ProfileSwitchScreen.kt @@ -42,6 +42,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.objects.extensions.getCustomizedName import app.aaps.core.objects.profile.ProfileSealed +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil @@ -249,7 +250,7 @@ private fun ProfileSwitchItem( if (profileSwitch.duration != null && profileSwitch.duration != 0L) { append(" ") append(T.msecs(profileSwitch.duration ?: 0L).mins().toInt()) - append(rh.gs(app.aaps.core.keys.R.string.units_min)) + append(rh.gs(CoreUiR.string.units_min)) } }, modifier = Modifier.padding(start = 4.dp), diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt index 0e95efe7716e..b16893665089 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempBasalScreen.kt @@ -43,6 +43,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.objects.extensions.iobCalc import app.aaps.core.objects.extensions.isInProgress +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil @@ -226,7 +227,7 @@ private fun TempBasalItem( append(" ") // Duration append(T.msecs(tempBasal.duration).mins().toInt().toString()) - append(stringResource(app.aaps.core.keys.R.string.units_min)) + append(stringResource(CoreUiR.string.units_min)) }, modifier = Modifier.padding(start = 4.dp), fontSize = 14.sp, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt index 729723e0ea90..b6ce16c865d7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt @@ -37,6 +37,7 @@ import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Translator import app.aaps.core.objects.extensions.highValueToUnitsToString import app.aaps.core.objects.extensions.lowValueToUnitsToString +import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil @@ -202,7 +203,7 @@ private fun TempTargetItem( append(" ") // Duration append(T.msecs(tempTarget.duration).mins().toInt()) - append(rh.gs(app.aaps.core.keys.R.string.units_min)) + append(rh.gs(CoreUiR.string.units_min)) append(" ") // Reason (without "Reason:" label) append(translator.translate(tempTarget.reason)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt index c674136c0250..f942b5b4413a 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt @@ -708,7 +708,7 @@ internal fun WizardDialogContent( onValueChange = onDirectCorrectionChange, valueRange = -uiState.maxBolus..uiState.maxBolus, step = uiState.bolusStep, - unitLabel = TextRef.Res(CoreUiR.string.insulin_unit_shortname), + unitLabel = TextRef.AndroidRes(CoreUiR.string.insulin_unit_shortname), decimalPlaces = 2, modifier = itemModifier ) diff --git a/ui/src/main/kotlin/app/aaps/ui/search/BuiltInSearchables.kt b/ui/src/main/kotlin/app/aaps/ui/search/BuiltInSearchables.kt index 4d0b76024db7..daee86eeba83 100644 --- a/ui/src/main/kotlin/app/aaps/ui/search/BuiltInSearchables.kt +++ b/ui/src/main/kotlin/app/aaps/ui/search/BuiltInSearchables.kt @@ -89,7 +89,7 @@ class BuiltInSearchables @Inject constructor( // Range settings subscreen PreferenceSubScreenDef( key = "range_settings", - titleResId = app.aaps.core.keys.R.string.prefs_range_title, + titleResId = app.aaps.core.ui.R.string.prefs_range_title, items = listOf( UnitDoubleKey.OverviewLowMark, UnitDoubleKey.OverviewHighMark From 7e735f6be2606924cb4dd21a5a84f30bb0c8130c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 8 Aug 2026 20:50:33 +0200 Subject: [PATCH 016/146] Update progress --- _docs/KMP_IOS_FEASIBILITY.md | 336 ++++++++++++++++++++++++++++++++--- 1 file changed, 316 insertions(+), 20 deletions(-) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 44abf7f5d9a4..d43522f9c0e6 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -408,15 +408,16 @@ Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Retrofit, OkHttp, `android.*` and `java.io` are all gone from it. -Step 2 is now half done as well: `:core:keys` no longer hands out bare resource ids, it hands out -`TextRef` (wave 10). The ids are still AAPT ids inside it, so this is the seam rather than the -move - -but it is the half that had to come first, because it is the half that touches every call site. +**Step 2 is prepared but not done.** `:core:keys` now hands out `TextRef` rather than bare resource +ids (wave 10), owns only strings it actually uses (wave 11), and nothing outside it reads its `R` +class (wave 12). The spike proved the toolchain works (wave 13). What has **not** happened is the +conversion itself - the module is still an Android library with `res/values*` and a generated `R`. +Resume instructions are in section 9b. -That leaves the rest of steps 2, 3, 4 and 6, and the half of step 0 that **needs a Mac**: a real -device, and an honest look at how Compose Multiplatform feels on iOS. No amount of further blocker -removal answers that question, which is the argument for doing it soon rather than continuing down -the list. +That leaves the rest of steps 2, 3, 4 and 6. Step 0's remaining half **needs a Mac** - or a +`macos-latest` CI runner, which is how the projects with public precedent do it and which is free for +a public repo. No amount of further blocker removal answers how Compose Multiplatform actually feels +on iOS. --- @@ -606,6 +607,13 @@ imports. The only blocker is **381 `R.string` references in 6 files** - the keys titles and summaries as raw `Int` resource ids. That is the resources job in section 6, not a separate problem. +> **That paragraph was wrong**, and it was wrong in a way worth remembering. It came from grepping +> `^import android` / `^import java`, which finds nothing when the type is written **fully qualified +> inline** or is **auto-imported**. Wave 11 found two real ones: +> `android.content.Context` in `StringPreferenceKey.kt` (written fully qualified, so no import line) +> and `java.lang.Class` in five places (`java.lang` needs no import). The same mistake shape recurred +> twice more later - see the note at the end of wave 12. Grep for *usage*, not for imports. + ### Wave 4 - `:core:data` ambient state (partly done) Two reads of ambient system state were removed from the models: @@ -1188,9 +1196,148 @@ resource-id-or-string pair that `TextRef` exists to collapse, and collapsing the `IntKeyWithEntries` and `withEntries` entirely. It is not in phase 1 because it touches plugin call sites, and phase 1 was scoped to `:core:*`. -`UnitType.valueResId()` / `rangeResId()` stay `Int?`. They are format templates that need arguments -supplied at the call site, they live in `:core:ui`, and they are only ever read from Compose - so -routing them through `TextRef` would add a hop and remove nothing. +`UnitType.valueResId()` / `rangeResId()` stay `Int?` for now. They are format templates that need +arguments supplied at the call site. (They lived in `:core:keys`, not `:core:ui` as an earlier +version of this note said. Wave 11 moved them.) + +### Wave 11 - `TextRef` phase 2, and `:core:keys` owns only its own strings (done) + +Committed as `95da0fa7ca` and `6fdb924e6b`. Three separate jobs that together leave `:core:keys` +with no raw Android resource id in its API and nothing in it that other modules reach into. + +#### The entries maps + +`IntPreferenceKey.entries: Map` and `StringPreferenceKey.entries: Map` became +`Map`. The 100 `value to R.string.x` pairs did **not** change: the same trick as +`title` - the constructor parameter turned private (`entriesResIds`) and a computed property exposes +it. 11 enums, 17 named arguments. + +`resolvedEntries` is **deleted**. It was the parallel `Map?` that took precedence over +`entries`, i.e. exactly the resource-id-or-string pair `TextRef` exists to collapse. `withEntries` +now takes `Map`, so callers choose `Res` or `Literal` per entry. Two read sites in +`AdaptivePreferenceItem` collapsed to one each. + +That change is what made a real fix possible: `EopatchPumpPlugin` built its reminder labels as +`"$it U"` and `"$it hr"` - string concatenation no translator can reach. They now use the existing +translated templates `units_format_insulin_int` and `units_format_hours`. Note the visible +consequence: expiration entries read "1 h", not "1 hr". Had `withEntries` kept `Map` and +wrapped in `Literal` internally, the bug would have been frozen in place. + +#### UnitType split + +`UnitType.kt` is now the enum plus `decimalPlaces()` and `step()` - zero resources, zero Android. The +resource mapping moved to `core/ui/.../UnitTypeText.kt` as `unitLabel(): TextRef?`, +`rangeText(value, min, max): TextRef?` (arguments taken here, so a bare template cannot reach the +screen) and `valueFormatResId(): Int?`, which stays an `Int` because the slider applies it to +whatever value the user drags to. + +#### The unitLabel pair, and an identity check on a resource id + +`formatSliderDisplayValue` took **both** `unitLabelResId: Int = 0` **and** `unitLabel: String = ""`. +One `unitLabel: TextRef?` replaced both, killing the sentinel and the pair, across ~26 files. + +Worse than the sentinel: three places detected a semantic concept by comparing resource ids - +`unitLabelResId == KeysR.string.units_min` in `FormatUtils`, `SliderWithButtons` and +`ValueInputDialog`. All three are now an explicit `asDuration: Boolean`. That kind of check is +exactly what breaks when ids stop being AAPT ids. + +#### `:core:keys` stops hosting other people's strings + +40 `units_*` strings moved to `:core:ui`, where 99 of their references already were - only **two** +were used from inside `:core:keys` (`units_mgdl`, `units_mmol` in `StringKey.GeneralUnits`), and they +became `TextRef.Literal("mg/dL")` / `TextRef.Literal("mmol/L")`, which is honest because they read +the same in every locale. `prefs_range_title` moved too (external-only). + +**The translations were moved by hand, per locale, in the same commit.** Not left to a later Crowdin +sync - see wave 12 for why that matters. + +Side effect worth knowing: after this move `:core:keys` has **zero format placeholders**, zero +plurals and zero string-arrays. Those are the categories that are hard build errors in +compose-resources, so the module that is about to convert is now the clean one. + +#### Deleted as dead + +`getDependingOn` had **no callers** anywhere - dependency visibility is computed reactively from +`preferenceKey.dependency` in `PreferenceState.kt`. Removed from the interface, both +`PreferencesImpl`s, the `PreviewUtils` stub and its one test. + +`rangeText` was **not** deleted, and the reasoning is worth recording. It looked dead - grep for +`min = Int.MIN_VALUE` finds nothing. But seven pump enums **default** `min`/`max` to the extreme +values, and three Insight constants take the default, so `hasValidRange` is false for them and the +branch runs. It returns null today only because those keys have no `unitType`. Same grep-the-explicit- +form mistake as wave 3. + +### Wave 12 - clearing the way for compose-resources (done, not yet converted) + +Two preparatory stages, both green, both on device. + +**Stage 1 - `TextRef.Res` renamed to `TextRef.AndroidRes`** across 187 sites. Pure rename. It frees +the name `Res` for the compose-resources form and, more importantly, states the truth: an Android +resource id is *one* kind of text reference, not the only one. Pump drivers keep AAPT resources +permanently, so two variants is the end state, not scaffolding. + +**Stage 2a - the `@StringRes Int` APIs take `TextRef`.** Five `:core:keys` strings were read as +Android ints by three other modules through APIs that cannot take a `StringResource`. Rather than +duplicate those strings (which would mean deleting them from Crowdin later - see wave 12's warning), +the APIs changed: + +| API | Change | +|----------------------------------|-------------------------------------------------------------------| +| `SWItem.label` | `Int?` -> `TextRef?`; `label(Int)` delegates | +| `SWScreen.header` | `Int = 0` sentinel -> `TextRef?`; `with(Int)` delegates | +| `SWEventListener.textLabel` | `Int = 0` sentinel -> `TextRef?` | +| `NumberInputRow.labelResId` | -> `labelRef: TextRef?`, `Int` overload keeps **79** call sites | +| `PasswordCheck.queryPassword` / `setPassword` | `TextRef` overloads; `Int` versions delegate | + +The delegating-overload trick is why this was ~5 call-site changes and not ~70. External callers now +ask the key for its own label - `StringKey.ProtectionMasterPassword.title` instead of +`R.string.master_password` - which also deleted a genuine pre-existing duplicate (`master_password` +and `pref_title_master_password` were both "Master password"). + +**Result: no module outside `:core:keys` references its `R` class.** That was the precondition. + +> **A recurring mistake, recorded so it stops happening.** Three times this wave I grepped for one +> syntactic form and concluded something was absent: `titleResId = 0` (missed the constructor +> *default*), `^import android` (missed fully-qualified and auto-imported use), `min = Int.MIN_VALUE` +> (missed the default again). Two of those nearly deleted live code. "I found no matches" is weak +> evidence, especially before a deletion. + +### Wave 13 - what the compose-resources spike proved + +A throwaway standalone project (not in the repo) answered the questions that decide the conversion. + +| Question | Answer | +|---|---| +| Does CMP resolve on Kotlin 2.4.10 / Gradle 9.6.1? | **Yes** - plugin `1.11.1`, `BUILD SUCCESSFUL`, generated a public `Res` | +| Do Android-style locale folders survive? | **Yes** - `values-de-rDE`, `values-pt-rBR`, `values-zh-rCN`, `values-iw-rIL` all parse into `LanguageQualifier` + `RegionQualifier`. **No renaming needed.** | +| Does `mingwX64` work? | **No.** `components-resources:1.11.1` publishes Native only for `ios_arm64`, `ios_simulator_arm64`, `macos_arm64` | +| Build cost? | baseline **15.8s**, +compose plugin **17.7s**, +328 strings x 31 locales **29.3s** | + +The build-cost split matters: the **Compose compiler plugin is ~2s**; the other **~11.6s is resource +codegen**, which scales with the number of locales, not with code. My worry that the IR transform +over the enums would be expensive was wrong. + +The `mingwX64` answer only matters because it was our Windows-buildable proxy for "compiles to +Native". The **real** target set is Android + iOS + JVM-on-Windows, and compose-resources supports +all three. So `mingwX64` is scaffolding we can drop for this module; the cost is that its Native side +can then only be compiled on macOS. + +Two corrections to earlier reasoning in this note: + +- `ResourceEnvironment`'s constructor is **public** in 1.11.1 (it was `internal` when wave 10 was + written). So an always-English `ResourceEnvironment(LanguageQualifier("en"), ...)` is possible, and + the search index's English half is **not** a blocker any more. That was the original reason for + rejecting `StringResource` on the key classes. +- Holding a `StringResource` costs **kotlin-stdlib only** - `components-resources` declares nothing + else in `apiElements`; Compose appears only in `runtimeElements`. + +Still true: **every `getString` overload is `suspend`.** There is no synchronous variant. + +There is real precedent on this exact toolchain: **Meshtastic-Android** runs a dedicated +`:core:resources` module (1747 strings, ~40 locales, `publicResClass = true`) on AGP 9.3.1 / +Kotlin 2.4.10 / CMP 1.11.1, and **Todometer-KMP** targets android + jvm + iosArm64 + +iosSimulatorArm64. Both build iOS only on `macos-latest` runners - which is the answer for a +maintainer with no Mac, and free for a public repo. ### Was behaviour preserved? @@ -1307,20 +1454,169 @@ keeps the old contract on a public interface method. because `getString()` is `suspend` and there is no public locale override, which would cost the English search index. `TextRef` is the seam that lets each module move on its own schedule without touching call sites twice. See wave 10. -15. **Still open: collapse `IntPreferenceKey.entries` / `resolvedEntries`.** Same - resource-id-or-string pair, and `TextRef` is exactly the type for it, but it reaches into plugin - call sites so it was left out of the `:core:*` phase. +15. ~~Collapse `IntPreferenceKey.entries` / `resolvedEntries`.~~ **Done in wave 11.** `entries` is + `Map`, `resolvedEntries` is deleted, `withEntries` takes `TextRef` - which is what + let the Eopatch `"$it U"` concatenation become a translated template. 16. ~~`RileyLinkStringPreferenceKey.MacAddress` should be a `NonPreferenceKey`.~~ **Done - moved to `RileyLinkStringKey`.** It is storage, not a preference: no title, no screen, written by the pairing wizard. With it gone the `titleResId = 0` default came out of all 26 enums, so a preference key without a title no longer compiles. See wave 10. +17. ~~Does `:core:keys` need its own strings?~~ **Yes, structurally.** It is a leaf module - zero + project dependencies - and `:core:ui` depends on **it**. So the strings cannot move to `:core:ui` + (cycle), and a key naming its own title at compile time means they must live in the same module. + That is what makes compose-resources the answer rather than a relocation. +18. ~~compose-resources on `:core:keys`, or negative-integer tokens in `TextRef`?~~ + **Decided: compose-resources.** The token scheme's whole advantage was keeping `mingwX64`, and + `mingwX64` is not in the real target set (Android + iOS + JVM-on-Windows). Tokens would also be a + bespoke design with no public precedent, which is the wrong trade when iOS cannot be compiled + locally. See wave 13. +19. **Still open: when to enable `MissingTranslation` lint.** It is disabled repo-wide and is the + reason 19 languages silently lost `:core:keys`. Needs to be on - at warning level with a CI + report - before any further string relocation. See section 9a. +20. **Still open: `:core:ui` is next after `:core:keys`.** It holds 1063 strings and every module + depends on it, so the same questions return at a larger scale. Worth deciding whether it converts + module-by-module or whether a dedicated shared-strings leaf is better at that point. + +Waves 1 to 4 are committed on `dev`. Waves 5 to 13 are committed on `kmp/core-data-experiment` +(HEAD `6fdb924e6b`), which is still **13+ commits ahead and 0 behind `dev` - a fast-forward**. + +Waves 5 to 9 were each verified against a live Nightscout before the next started - waves 5 and 6 on +WSA, waves 7 to 9 on an emulator running a new master against a **pre-KMP client**, so every step was +checked in a mixed-version pair. Waves 10 to 12 are compile-time refactors with no wire format in +them, and were verified on the emulator by reading the UI: preference screens, nested sections, +search, the client's sync badges, twelve setup-wizard screens, and the treatment dialogs. The failure +mode of these waves is a **blank label**, not a crash, so they need eyes rather than logcat. + +Still unverified at runtime, because they need hardware the emulator cannot provide: the Eopatch and +Medtrum entry lists, and the Insight `rangeText` path. + +The `FoodManagement` comma defect in section 10 is found but not fixed. + +--- + +## 9a. LIVE BUG - `:core:keys` translations are missing in 19 of 30 languages + +**This is on `dev` today, it predates all KMP work, and nothing in the build can see it.** + +`core/keys/src/main/res/values/strings.xml` has 327 strings. Only **11 locales** have translations: +bg, cs, es, fr, it, nb, ro, sk, vi, zh-rCN, zh-rTW. The other **19 locale files exist but contain +zero `` elements**: de, ru, pl, nl, pt-rBR, pt-rPT, tr, sv, uk, ar, ca, da, el, hr, hu, iw, +ko, lt, sr. + +The proof it is a regression, not merely untranslated work: +`core/ui/src/main/res/values-de-rDE/strings.xml` still carries the marker +``. The German text was removed from +`:core:ui` when the string moved, and never arrived. German users see these strings in English. + +**Why nothing caught it:** `buildSrc/.../android-module-dependencies.gradle.kts:30-31` does +`disable += "MissingTranslation"` and `disable += "ExtraTranslation"` for **every** Android library, +and CI runs only `:app:assemble` / `:wear:assemble` - no lint, no tests. There is currently no +mechanism in this repo that can notice a lost string. + +### What was already done about it + +A Crowdin **TM pre-translate** was run against `fileId 5662` (`/dev/core/keys/.../strings.xml`) for +all 19 languages, with `translateUntranslatedOnly: true` and `autoApproveOption: "none"`. + +| | before | after | +|---|---|---| +| de | 18 | 150 | +| ru | 40 | 176 | +| pl / tr | 0 | 150 | +| nl | 11 | 157 | +| ... | | typically 100-150 of 327 | + +**Recovery is partial (~40%), and this tells us something.** If all these strings had simply *moved*, +TM would have matched nearly all of them. It did not - so roughly 60% were **never translated in any +locale**. It is two problems: a genuine relocation loss, sitting on a larger backlog of new strings +from the Compose preferences migration. + +**Nothing is approved.** The project exports approved translations only, so a `download` still +returns empty files until someone approves in the web editor. The entries are also fuzzy matches +(`translateWithPerfectMatchOnly: false`), which is why they were left for review. + +### Why this blocks the string move + +Phase 3 relocates 327 strings x 30 locales. Right now you **cannot tell a relocation failure from +the pre-existing gap** - for 19 languages there is nothing left to break. Fix and verify first, then +move. Concretely, before converting: + +1. Approve the pre-translated suggestions (or decide to discard them). +2. Turn `MissingTranslation` back on - **warning level plus a CI report**, not an error gate, or the + first run is unusable. +3. Re-download and commit, so the repo has a known-good baseline. + +Useful commands are in the session notes: the CLI needs `-b dev`, a minimal temp `crowdin.yml` with +`preserve_hierarchy: true`, and `--base-path`. The API route avoids the export build entirely: +`GET /api/v2/projects/309752/languages/{lang}/translations?fileId=5662`. + +--- -Waves 1 to 4 are committed on `dev`. Waves 5 to 9 are committed on `kmp/core-data-experiment`, each -verified against a live Nightscout before the next one started - waves 5 and 6 on WSA, waves 7 to 9 -on an emulator running a new master against a **pre-KMP client**, so every step was checked in a -mixed-version pair rather than only against itself. Wave 10 is on the same branch and is not on a -device yet - it is a compile time refactor with no wire format in it, so the risk is different in -kind from waves 5 to 9. The `FoodManagement` comma defect in section 10 is found but not fixed. +## 9b. Where to resume - `:core:keys` to compose-resources + +Everything up to here is committed. The conversion itself has **not** started. + +### Stage 2b - convert the module + +1. **`core/keys/build.gradle.kts`** - replace the Android-library setup with: + `kotlin("multiplatform")` + `org.jetbrains.compose` + `org.jetbrains.kotlin.plugin.compose` + (the Compose plugin **hard-fails** without the compiler plugin) + `com.android.library`. + Targets: `androidTarget()`, `jvm()`, `iosArm64()`, `iosSimulatorArm64()`. **No `mingwX64`** - + `components-resources` does not publish it. + `compose.resources { publicResClass = true; packageOfResClass = "app.aaps.core.keys.resources"; + generateResClass = always }`, and `android { androidResources.enable = true }`. + Add `api(compose.components.resources)` - `api`, because the type is in the public API. +2. **Move sources** `src/main/kotlin` -> `src/commonMain/kotlin` (47 files). +3. **Move resources** `src/main/res/values*/strings.xml` -> + `src/commonMain/composeResources/values*/strings.xml`. **Folder names stay exactly as they are** - + the spike proved `values-de-rDE` etc. parse correctly. +4. **Add `TextRef.Res(resource: StringResource, args: List)`** next to `AndroidRes` and + `Literal`. +5. **~340 `R.string.x` -> `Res.string.x`** in the 6 enum files. The generated class cannot be named + `R`, so this is a real edit, not a package alias. +6. Watch `minSdk = min(Versions.minSdk, Versions.wearMinSdk)` - `:wear` consumes this module, and a + KMP module with `androidTarget()` consumed by `:wear` is the part with least public precedent. + +### Stage 3 - resolvers + +- Compose: `stringResource(TextRef)` in `:core:ui` gains a `Res` branch. +- Non-Compose: `ResourceHelper.gs(TextRef)` must stay **synchronous** because ~15 call sites and the + search index are not suspend. `getString` is suspend, so use a **lazy cache**: + `ConcurrentHashMap`, filled with `getOrPut { runBlocking { getString(...) } }`, + so the blocking read happens at most once per string. Pair it with a **background warm** at startup + so the main thread rarely pays it. A second cache keyed on the English `ResourceEnvironment` serves + `gsNotLocalised` for the search index. +- Do **not** make `SearchIndexBuilder.getIndex()` suspend - the cache removes the need, and the + suspend route would spread to ~15 other synchronous callers. + +### Follow-ups, in rough priority order + +1. **Merge `kmp/core-data-experiment` into `dev`** - still a fast-forward, and it gets less free + every day. (Open decision 11.) +2. **The translation work in section 9a** - before any further string move. +3. **A `macos-latest` CI job** building `iosSimulatorArm64`. Nothing verifies the Native side today; + CI is all `ubuntu-latest` running only `:app:assemble`. +4. **`PluginDescription.description: Int`** - the `-1` sentinel, set by 57 files. Must become + `TextRef` before plugins convert. +5. **Remaining `!= 0` / `!= -1` resource sentinels** - `SearchableItem` (2), `MainDrawer`, + `ManageBottomSheet`, `TreatmentBottomSheet`, `PreferenceScreenView`, `QuickLaunchResolver`, + `InfoStep`, `SWEventListener`. Harmless today. +6. **`IntPreferenceKey.entries` in the pump modules** still use `entriesResIds`; fine while pumps stay + Android. +7. **Swap `mingwX64` for Apple targets in `:core:data` / `:core:nssdk`** once macOS CI exists - they + currently prove themselves against a platform we do not ship. + +### Environment notes for another machine + +- **`:wear:kspFullDebugKotlin` fails on the first run of almost every build** with + `this and base files have different roots: C:\...\dagger-2.60.1.jar!... and E:\Github\AndroidAPS3\wear`. + It is **not flaky** - it is KSP relativising a path from the Gradle cache on `C:` against a project + on `E:`. A re-run succeeds because the output is cached. Fix properly by putting `GRADLE_USER_HOME` + on the same drive as the project. +- **`:pump:combov2:comboctl` and `:pump:danars-emulator`** have async tests that time out under a + fully parallel build but pass when run alone. Verified twice each. Do not chase them. +- Use redirect, not pipe, for gradle output - a pipe reports the *pipe's* exit code, so a failed + build looks like it passed. --- From c8c106981776bfcf91e27cc3c3c9e1c7daceba52 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 16:01:56 +0200 Subject: [PATCH 017/146] Fix Danish language selection --- .../main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt index 3d2ab69c8f1e..42b98d42cbd4 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt @@ -19,6 +19,13 @@ object LocaleHelper { // injection not possible because of use in attachBaseContext //preferences.get(R.string.key_language, Locale.getDefault().language) + // The language list has offered "dk" for Danish from the start, but "dk" is a country code, + // not a language code. The ISO 639 code is "da", and every Danish translation lives in a + // values-da-rDK folder. A stored "dk" matched no folder, so picking Danish showed the whole + // app in English. The stored value is left as it is, because it is a preference that syncs + // between master and client; it is corrected here, where the Locale is built. + private fun isoLanguage(language: String): String = if (language == "dk") "da" else language + fun currentLocale(context: Context): Locale { val language = selectedLanguage(context) if (language == "default") return Locale.getDefault() @@ -29,7 +36,7 @@ object LocaleHelper { val country = language.substring(3, 5) Locale.Builder().setLanguage(lang).setRegion(country).build() } else { - Locale.Builder().setLanguage(language).build() + Locale.Builder().setLanguage(isoLanguage(language)).build() } } From 29dd6ff33ee8bd08c50cb27bc9d421e818a61f69 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 16:02:01 +0200 Subject: [PATCH 018/146] :core:keys TextRef.Named --- .../src/main/kotlin/GenerateKeyStringsTask.kt | 225 ++++++++++++++++++ .../interfaces/resources/ResourceHelper.kt | 29 ++- core/keys/build.gradle.kts | 29 +++ .../kotlin/app/aaps/core/keys/BooleanKey.kt | 202 ++++++++-------- .../kotlin/app/aaps/core/keys/DoubleKey.kt | 126 +++++----- .../main/kotlin/app/aaps/core/keys/IntKey.kt | 166 +++++++------ .../kotlin/app/aaps/core/keys/IntentKey.kt | 6 +- .../kotlin/app/aaps/core/keys/StringKey.kt | 142 ++++++----- .../app/aaps/core/keys/UnitDoubleKey.kt | 14 +- .../app/aaps/core/keys/interfaces/TextRef.kt | 29 ++- .../aaps/core/ui/compose/TextRefResource.kt | 25 +- 11 files changed, 640 insertions(+), 353 deletions(-) create mode 100644 buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt diff --git a/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt b/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt new file mode 100644 index 000000000000..46e794bebd8f --- /dev/null +++ b/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt @@ -0,0 +1,225 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Turns a module's `res/values/strings.xml` into two generated Kotlin files, so preference keys can + * name a string without naming an Android resource id. + * + * Why this exists: a key enum that says `titleResId = R.string.x` carries an `Int` that only means + * something on Android, which is what stops the module compiling for iOS or a desktop JVM. The + * generated `KeysStrings` object replaces that `Int` with a `TextRef.Named("x")` - a plain string + * name that every platform can hold - while the generated id map keeps Android resolving through + * AAPT exactly as before. + * + * The pair is generated from the same XML in one pass, so the two sides cannot drift: a string + * deleted from `strings.xml` disappears from both, and any call site that still names it stops + * compiling. + * + * This is deliberately not `Resources.getIdentifier()`. That is a reflective lookup R8 cannot see, + * it would keep every string alive, and a typo would silently return 0. A generated map is plain + * code: R8 sees literal `R.string.x` references, and a typo is a compile error. + * + * It also reports translation completeness per locale, which nothing else in this build does - + * `MissingTranslation` and `ExtraTranslation` lint are disabled for every library module. + */ +abstract class GenerateKeyStringsTask : DefaultTask() { + + /** The module's `src/main/res` (or `src/androidMain/res`) directory. */ + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val resDir: DirectoryProperty + + /** Package for both generated files. Must match the module's namespace, so `R` resolves. */ + @get:Input + abstract val packageName: Property + + /** Name of the platform neutral object, for example `KeysStrings`. */ + @get:Input + abstract val objectName: Property + + /** Name of the Android id map object, for example `KeysStringIds`. */ + @get:Input + abstract val idsObjectName: Property + + /** Holds the generated object of [TextRef] names. Platform neutral - no Android types. */ + @get:OutputDirectory + abstract val commonOutputDir: DirectoryProperty + + /** Holds the generated name to `R.string` id map. Android only. */ + @get:OutputDirectory + abstract val androidOutputDir: DirectoryProperty + + /** Per locale completeness report. */ + @get:OutputFile + abstract val reportFile: RegularFileProperty + + @TaskAction + fun generate() { + val res = resDir.get().asFile + val baseFile = File(res, "values/strings.xml") + if (!baseFile.isFile) throw GradleException("No values/strings.xml under $res") + + val names = readStringNames(baseFile) + if (names.isEmpty()) throw GradleException("No elements in $baseFile") + + val duplicates = names.groupBy { it }.filterValues { it.size > 1 }.keys + if (duplicates.isNotEmpty()) { + throw GradleException("Duplicate string names in $baseFile: ${duplicates.sorted()}") + } + + val unsafe = names.filterNot { it.isSafeKotlinIdentifier() } + if (unsafe.isNotEmpty()) { + // A name that is not a valid identifier cannot become a property on the generated + // object. Renaming it in strings.xml is the fix, but that also renames it in Crowdin, + // so it has to be a deliberate act rather than something the generator papers over. + throw GradleException( + "These string names cannot be generated as Kotlin properties: ${unsafe.sorted()}. " + + "Rename them in $baseFile." + ) + } + + writeCommon(names.sorted()) + writeAndroid(names.sorted()) + writeReport(res, names.toSet()) + } + + private fun writeCommon(names: List) { + val dir = commonOutputDir.get().asFile + dir.deleteRecursively() + val pkg = packageName.get() + val obj = objectName.get() + val file = File(dir, pkg.replace('.', '/') + "/$obj.kt") + file.parentFile.mkdirs() + file.writeText( + buildString { + append(GENERATED_HEADER) + append("package $pkg\n\n") + append("import app.aaps.core.keys.interfaces.TextRef\n\n") + append("/**\n") + append(" * Every string this module owns, as a platform neutral [TextRef].\n") + append(" *\n") + append(" * Generated from `res/values/strings.xml`. Use these instead of `R.string.*` so the\n") + append(" * declaring code stays free of Android resource ids.\n") + append(" */\n") + append("object $obj {\n\n") + names.forEach { append(" val $it: TextRef = TextRef.Named(\"$it\")\n") } + append("}\n") + } + ) + } + + private fun writeAndroid(names: List) { + val dir = androidOutputDir.get().asFile + dir.deleteRecursively() + val pkg = packageName.get() + val obj = idsObjectName.get() + val named = objectName.get() + val file = File(dir, pkg.replace('.', '/') + "/$obj.kt") + file.parentFile.mkdirs() + file.writeText( + buildString { + append(GENERATED_HEADER) + append("package $pkg\n\n") + append("/**\n") + append(" * Maps the names in [$named] back to this module's AAPT resource ids.\n") + append(" *\n") + append(" * This is what keeps Android on its own resource system: a `TextRef.Named` is resolved to\n") + append(" * an `R.string` id here and then read through `Resources`, so locale matching, the\n") + append(" * always English lookup and `MissingTranslation` lint all behave exactly as they did when\n") + append(" * the keys carried ids directly.\n") + append(" */\n") + append("object $obj {\n\n") + append(" private val ids: Map = mapOf(\n") + names.forEach { append(" \"$it\" to R.string.$it,\n") } + append(" )\n\n") + append(" /**\n") + append(" * The resource id for [name], or null when this module does not own that name.\n") + append(" *\n") + append(" * Null means someone built a `TextRef.Named` by hand instead of taking it from\n") + append(" * [$named]. Callers show the raw name rather than crashing, which makes the mistake\n") + append(" * visible on screen without taking the app down.\n") + append(" */\n") + append(" fun idOf(name: String): Int? = ids[name]\n") + append("}\n") + } + ) + } + + private fun writeReport(res: File, expected: Set) { + val report = StringBuilder() + report.append("Translation completeness for ${res.parentFile.parentFile.parentFile.name}\n") + report.append("Base locale has ${expected.size} strings.\n\n") + + val locales = res.listFiles() + ?.filter { it.isDirectory && it.name.startsWith("values-") } + ?.sortedBy { it.name } + .orEmpty() + + var empty = 0 + var partial = 0 + locales.forEach { dir -> + val file = File(dir, "strings.xml") + val present = if (file.isFile) readStringNames(file).toSet() else emptySet() + val missing = expected - present + val extra = present - expected + val state = when { + present.isEmpty() -> { empty++; "EMPTY" } + missing.isEmpty() && extra.isEmpty() -> "complete" + else -> { partial++; "PARTIAL" } + } + report.append("%-18s %4d/%d %s\n".format(dir.name, present.size, expected.size, state)) + if (extra.isNotEmpty()) report.append(" not in base locale: ${extra.sorted()}\n") + } + + report.append("\n${locales.size} locales: ${locales.size - empty - partial} complete, $partial partial, $empty empty.\n") + + val out = reportFile.get().asFile + out.parentFile.mkdirs() + out.writeText(report.toString()) + + // A warning, not a failure. 19 of these locales are empty today for reasons that predate + // this task, so failing here would make the build red on the first run. The point is that + // the number is now visible and can be tightened later. + if (empty > 0 || partial > 0) { + logger.warn("$name: $partial partial and $empty empty locales - see ${out.absolutePath}") + } + } + + private fun readStringNames(file: File): List { + val doc = DocumentBuilderFactory.newInstance() + .apply { isNamespaceAware = false } + .newDocumentBuilder() + .parse(file) + val nodes = doc.getElementsByTagName("string") + return (0 until nodes.length).mapNotNull { i -> + nodes.item(i).attributes?.getNamedItem("name")?.nodeValue + } + } + + private fun String.isSafeKotlinIdentifier(): Boolean = + matches(Regex("[A-Za-z_][A-Za-z0-9_]*")) && this !in KOTLIN_KEYWORDS + + private companion object { + + const val GENERATED_HEADER = + "// Generated by GenerateKeyStringsTask from res/values/strings.xml. Do not edit.\n\n" + + val KOTLIN_KEYWORDS = setOf( + "as", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", + "interface", "is", "null", "object", "package", "return", "super", "this", "throw", + "true", "try", "typealias", "typeof", "val", "var", "when", "while" + ) + } +} diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index abc8b173ab2b..a1736a800faf 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.RawRes import androidx.annotation.StringRes +import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef interface ResourceHelper { @@ -25,21 +26,35 @@ interface ResourceHelper { * Resolves a [TextRef] outside Compose. Inside Compose use * `app.aaps.core.ui.compose.stringResource` instead. * - * Every non-Compose reader of a preference title goes through here, which is the point: when a - * module later moves its strings out of `res/values`, only this one method has to learn about - * the new form of [TextRef.AndroidRes]. + * Every non-Compose reader of a preference title goes through here, which is the point: a module + * that stops naming resource ids in its own code changes only this method, not its call sites. + * + * [TextRef.Named] still ends up in `Resources` on Android - the name is turned into an id first - + * so locale matching behaves exactly as it does for [TextRef.AndroidRes]. */ fun gs(ref: TextRef): String = when (ref) { - is TextRef.Literal -> ref.text - is TextRef.AndroidRes -> + is TextRef.Literal -> ref.text + is TextRef.AndroidRes -> if (ref.args.isEmpty()) gs(ref.id) else gs(ref.id, *ref.args.toTypedArray()) + + is TextRef.Named -> { + val id = KeysStringIds.idOf(ref.name) + when { + id == null -> ref.name + ref.args.isEmpty() -> gs(id) + else -> gs(id, *ref.args.toTypedArray()) + } + } } /** Same, but always in English - used to build the search index. */ fun gsNotLocalised(ref: TextRef): String = when (ref) { - is TextRef.Literal -> ref.text - is TextRef.AndroidRes -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) + is TextRef.Literal -> ref.text + is TextRef.AndroidRes -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) + is TextRef.Named -> KeysStringIds.idOf(ref.name) + ?.let { gsNotLocalised(it, *ref.args.toTypedArray()) } + ?: ref.name } @ColorInt fun gc(@ColorRes id: Int): Int diff --git a/core/keys/build.gradle.kts b/core/keys/build.gradle.kts index 5cb050f2a824..44327cb9b65b 100644 --- a/core/keys/build.gradle.kts +++ b/core/keys/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min plugins { @@ -15,6 +16,34 @@ android { } } +// The key enums name their titles through the generated KeysStrings object rather than R.string, +// so this module stops carrying Android resource ids in its public API. KeysStringIds keeps the +// Android side resolving through AAPT. Both files come from one pass over res/values/strings.xml, +// so they cannot drift apart. See GenerateKeyStringsTask for the reasoning. +extensions.configure("androidComponents") { + onVariants { variant -> + val taskProvider = tasks.register( + "generate${variant.name.replaceFirstChar { it.uppercase() }}KeyStrings", + GenerateKeyStringsTask::class.java + ) { + resDir.set(layout.projectDirectory.dir("src/main/res")) + packageName.set("app.aaps.core.keys") + objectName.set("KeysStrings") + idsObjectName.set("KeysStringIds") + reportFile.set(layout.buildDirectory.file("reports/keyStrings/${variant.name}-translations.txt")) + // Set explicitly. addGeneratedSourceDirectory only applies a convention, and it derives + // that convention from the task name, so both properties would land on the same + // directory and the second file written would delete the first. + commonOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/${variant.name}/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/${variant.name}/android")) + } + // Two directories rather than one: the names are platform neutral and will move to + // commonMain when this module becomes multiplatform, while the id map stays on Android. + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) + } +} + dependencies { api(platform(libs.kotlinx.coroutines.bom)) api(libs.kotlinx.coroutines.core) diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt index 748dadc226dc..3ccc46f9364d 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt @@ -11,8 +11,8 @@ import app.aaps.core.keys.interfaces.TextRef enum class BooleanKey( override val key: String, override val defaultValue: Boolean, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.SWITCH, override val calculatedDefaultValue: Boolean = false, override val defaultedBySM: Boolean = false, @@ -29,92 +29,92 @@ enum class BooleanKey( override val sync: SyncSpec? = null ) : BooleanPreferenceKey { - GeneralSimpleMode(key = "simple_mode", defaultValue = true, titleResId = R.string.pref_title_simple_mode, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + GeneralSimpleMode(key = "simple_mode", defaultValue = true, title = KeysStrings.pref_title_simple_mode, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), GeneralInsulinConcentration( - key = "insulin_concentration_enabled", defaultValue = false, titleResId = R.string.pref_title_insulin_concentration, summaryResId = R.string.pref_summary_insulin_concentration, + key = "insulin_concentration_enabled", defaultValue = false, title = KeysStrings.pref_title_insulin_concentration, summary = KeysStrings.pref_summary_insulin_concentration, defaultedBySM = true, enabledCondition = PreferenceEnabledCondition { it.isConcentrationEnabled }, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - OverviewKeepScreenOn(key = "keep_screen_on", defaultValue = false, titleResId = R.string.pref_title_keep_screen_on, summaryResId = R.string.pref_summary_keep_screen_on, calculatedDefaultValue = true), - OverviewShowTreatmentButton(key = "show_treatment_button", defaultValue = false, titleResId = R.string.pref_title_show_treatment_button, defaultedBySM = true), - OverviewShowWizardButton(key = "show_wizard_button", defaultValue = true, titleResId = R.string.pref_title_show_wizard_button, defaultedBySM = true), - OverviewShowInsulinButton(key = "show_insulin_button", defaultValue = true, titleResId = R.string.pref_title_show_insulin_button, defaultedBySM = true), - OverviewShowCarbsButton(key = "show_carbs_button", defaultValue = true, titleResId = R.string.pref_title_show_carbs_button, defaultedBySM = true), - OverviewShowCgmButton(key = "show_cgm_button", defaultValue = false, titleResId = R.string.pref_title_show_cgm_button, summaryResId = R.string.pref_summary_show_cgm_button, defaultedBySM = true, showInNsClientMode = false), + OverviewKeepScreenOn(key = "keep_screen_on", defaultValue = false, title = KeysStrings.pref_title_keep_screen_on, summary = KeysStrings.pref_summary_keep_screen_on, calculatedDefaultValue = true), + OverviewShowTreatmentButton(key = "show_treatment_button", defaultValue = false, title = KeysStrings.pref_title_show_treatment_button, defaultedBySM = true), + OverviewShowWizardButton(key = "show_wizard_button", defaultValue = true, title = KeysStrings.pref_title_show_wizard_button, defaultedBySM = true), + OverviewShowInsulinButton(key = "show_insulin_button", defaultValue = true, title = KeysStrings.pref_title_show_insulin_button, defaultedBySM = true), + OverviewShowCarbsButton(key = "show_carbs_button", defaultValue = true, title = KeysStrings.pref_title_show_carbs_button, defaultedBySM = true), + OverviewShowCgmButton(key = "show_cgm_button", defaultValue = false, title = KeysStrings.pref_title_show_cgm_button, summary = KeysStrings.pref_summary_show_cgm_button, defaultedBySM = true, showInNsClientMode = false), OverviewShowCalibrationButton( key = "show_calibration_button", defaultValue = false, - titleResId = R.string.pref_title_show_calibration_button, - summaryResId = R.string.pref_summary_show_calibration_button, + title = KeysStrings.pref_title_show_calibration_button, + summary = KeysStrings.pref_summary_show_calibration_button, defaultedBySM = true, showInNsClientMode = false ), - OverviewShowNotesInDialogs(key = "show_notes_entry_dialogs", defaultValue = false, titleResId = R.string.pref_title_show_notes_in_dialogs, defaultedBySM = true), - OverviewUseBolusAdvisor("use_bolus_advisor", true, R.string.pref_title_use_bolus_advisor, R.string.pref_summary_use_bolus_advisor, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), - OverviewUseBolusReminder("use_bolus_reminder", true, R.string.pref_title_use_bolus_reminder, R.string.pref_summary_use_bolus_reminder, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + OverviewShowNotesInDialogs(key = "show_notes_entry_dialogs", defaultValue = false, title = KeysStrings.pref_title_show_notes_in_dialogs, defaultedBySM = true), + OverviewUseBolusAdvisor("use_bolus_advisor", true, KeysStrings.pref_title_use_bolus_advisor, KeysStrings.pref_summary_use_bolus_advisor, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + OverviewUseBolusReminder("use_bolus_reminder", true, KeysStrings.pref_title_use_bolus_reminder, KeysStrings.pref_summary_use_bolus_reminder, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), @Deprecated("Remove support") - OverviewUseSuperBolus("key_usersuperbolus", false, R.string.pref_title_use_super_bolus, R.string.pref_summary_use_super_bolus, defaultedBySM = true, hideParentScreenIfHidden = true), + OverviewUseSuperBolus("key_usersuperbolus", false, KeysStrings.pref_title_use_super_bolus, KeysStrings.pref_summary_use_super_bolus, defaultedBySM = true, hideParentScreenIfHidden = true), PumpBtWatchdog( - "bt_watchdog", false, R.string.pref_title_bt_watchdog, R.string.pref_summary_bt_watchdog, + "bt_watchdog", false, KeysStrings.pref_title_bt_watchdog, KeysStrings.pref_summary_bt_watchdog, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - AlertMissedBgReading("enable_missed_bg_readings", false, R.string.pref_title_alert_missed_bg_reading), - AlertPumpUnreachable("enable_pump_unreachable_alert", true, R.string.pref_title_alert_pump_unreachable), - AlertCarbsRequired("enable_carbs_required_alert_local", true, R.string.pref_title_alert_carbs_required), - AlertUrgentAsAndroidNotification("raise_urgent_alarms_as_android_notification", true, R.string.pref_title_alert_urgent_as_android_notification), - AlertIncreaseVolume("gradually_increase_notification_volume", true, R.string.pref_title_alert_increase_volume), - AlertOverrideDoNotDisturb("alert_override_dnd", true, R.string.pref_title_alert_override_dnd, R.string.pref_summary_alert_override_dnd, defaultedBySM = true), + AlertMissedBgReading("enable_missed_bg_readings", false, KeysStrings.pref_title_alert_missed_bg_reading), + AlertPumpUnreachable("enable_pump_unreachable_alert", true, KeysStrings.pref_title_alert_pump_unreachable), + AlertCarbsRequired("enable_carbs_required_alert_local", true, KeysStrings.pref_title_alert_carbs_required), + AlertUrgentAsAndroidNotification("raise_urgent_alarms_as_android_notification", true, KeysStrings.pref_title_alert_urgent_as_android_notification), + AlertIncreaseVolume("gradually_increase_notification_volume", true, KeysStrings.pref_title_alert_increase_volume), + AlertOverrideDoNotDisturb("alert_override_dnd", true, KeysStrings.pref_title_alert_override_dnd, KeysStrings.pref_summary_alert_override_dnd, defaultedBySM = true), - BgSourceUploadToNs("dexcomg5_nsupload", true, R.string.pref_title_bg_source_upload_to_ns, defaultedBySM = true, hideParentScreenIfHidden = true), - BgSourceCreateSensorChange("dexcom_lognssensorchange", true, R.string.pref_title_bg_source_create_sensor_change, R.string.pref_summary_bg_source_create_sensor_change, defaultedBySM = true), - BgSourceRandomBgRandomize("randombg_randomize", true, R.string.pref_title_random_bg_randomize, R.string.pref_summary_random_bg_randomize, defaultedBySM = true), + BgSourceUploadToNs("dexcomg5_nsupload", true, KeysStrings.pref_title_bg_source_upload_to_ns, defaultedBySM = true, hideParentScreenIfHidden = true), + BgSourceCreateSensorChange("dexcom_lognssensorchange", true, KeysStrings.pref_title_bg_source_create_sensor_change, KeysStrings.pref_summary_bg_source_create_sensor_change, defaultedBySM = true), + BgSourceRandomBgRandomize("randombg_randomize", true, KeysStrings.pref_title_random_bg_randomize, KeysStrings.pref_summary_random_bg_randomize, defaultedBySM = true), - ApsUseDynamicSensitivity("use_dynamic_sensitivity", false, R.string.pref_title_aps_use_dynamic_sensitivity, R.string.pref_summary_aps_use_dynamic_sensitivity, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsUseDynamicSensitivity("use_dynamic_sensitivity", false, KeysStrings.pref_title_aps_use_dynamic_sensitivity, KeysStrings.pref_summary_aps_use_dynamic_sensitivity, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsUseAutosens( - "openapsama_useautosens", true, R.string.pref_title_aps_use_autosens, defaultedBySM = true, + "openapsama_useautosens", true, KeysStrings.pref_title_aps_use_autosens, defaultedBySM = true, // Hidden only while the active APS both offers dynamic sensitivity and has it enabled. // A plain negativeDependency on ApsUseDynamicSensitivity would also hide it on algorithms // whose screens never show that toggle (AMA, AutoISF), with no way to reveal it (issue #4482). visibility = ElementVisibility { !(it.apsOffersDynamicSensitivity && it.preferences.get(ApsUseDynamicSensitivity)) }, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ApsUseSmb("use_smb", true, R.string.pref_title_aps_use_smb, R.string.pref_summary_aps_use_smb, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsUseSmb("use_smb", true, KeysStrings.pref_title_aps_use_smb, KeysStrings.pref_summary_aps_use_smb, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsUseSmbWithHighTt( "enableSMB_with_high_temptarget", false, - R.string.pref_title_aps_use_smb_with_high_tt, - R.string.pref_summary_aps_use_smb_with_high_tt, + KeysStrings.pref_title_aps_use_smb_with_high_tt, + KeysStrings.pref_summary_aps_use_smb_with_high_tt, defaultedBySM = true, dependency = ApsUseSmb, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsUseSmbAlways( - "enableSMB_always", true, R.string.pref_title_aps_use_smb_always, R.string.pref_summary_aps_use_smb_always, defaultedBySM = true, dependency = ApsUseSmb, + "enableSMB_always", true, KeysStrings.pref_title_aps_use_smb_always, KeysStrings.pref_summary_aps_use_smb_always, defaultedBySM = true, dependency = ApsUseSmb, visibility = ElementVisibility.ADVANCED_FILTERING, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsUseSmbWithCob( - "enableSMB_with_COB", true, R.string.pref_title_aps_use_smb_with_cob, R.string.pref_summary_aps_use_smb_with_cob, defaultedBySM = true, dependency = ApsUseSmb, + "enableSMB_with_COB", true, KeysStrings.pref_title_aps_use_smb_with_cob, KeysStrings.pref_summary_aps_use_smb_with_cob, defaultedBySM = true, dependency = ApsUseSmb, visibility = ElementVisibility { !it.preferences.get(ApsUseSmbAlways) || !it.advancedFilteringSupported }, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsUseSmbWithLowTt( - "enableSMB_with_temptarget", true, R.string.pref_title_aps_use_smb_with_low_tt, R.string.pref_summary_aps_use_smb_with_low_tt, defaultedBySM = true, dependency = ApsUseSmb, + "enableSMB_with_temptarget", true, KeysStrings.pref_title_aps_use_smb_with_low_tt, KeysStrings.pref_summary_aps_use_smb_with_low_tt, defaultedBySM = true, dependency = ApsUseSmb, visibility = ElementVisibility { !it.preferences.get(ApsUseSmbAlways) || !it.advancedFilteringSupported }, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsUseSmbAfterCarbs( - "enableSMB_after_carbs", true, R.string.pref_title_aps_use_smb_after_carbs, R.string.pref_summary_aps_use_smb_after_carbs, defaultedBySM = true, dependency = ApsUseSmb, + "enableSMB_after_carbs", true, KeysStrings.pref_title_aps_use_smb_after_carbs, KeysStrings.pref_summary_aps_use_smb_after_carbs, defaultedBySM = true, dependency = ApsUseSmb, visibility = ElementVisibility { !it.preferences.get(ApsUseSmbAlways) && it.advancedFilteringSupported }, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ApsUseUam("use_uam", true, R.string.pref_title_aps_use_uam, R.string.pref_summary_aps_use_uam, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsUseUam("use_uam", true, KeysStrings.pref_title_aps_use_uam, KeysStrings.pref_summary_aps_use_uam, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsSensitivityRaisesTarget( - "sensitivity_raises_target", true, R.string.pref_title_aps_sensitivity_raises_target, R.string.pref_summary_aps_sensitivity_raises_target, defaultedBySM = true, + "sensitivity_raises_target", true, KeysStrings.pref_title_aps_sensitivity_raises_target, KeysStrings.pref_summary_aps_sensitivity_raises_target, defaultedBySM = true, visibility = ElementVisibility { if (it.preferences.get(ApsUseDynamicSensitivity)) { it.preferences.get(ApsDynIsfAdjustSensitivity) @@ -125,7 +125,7 @@ enum class BooleanKey( sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsResistanceLowersTarget( - "resistance_lowers_target", true, R.string.pref_title_aps_resistance_lowers_target, R.string.pref_summary_aps_resistance_lowers_target, defaultedBySM = true, + "resistance_lowers_target", true, KeysStrings.pref_title_aps_resistance_lowers_target, KeysStrings.pref_summary_aps_resistance_lowers_target, defaultedBySM = true, visibility = ElementVisibility { if (it.preferences.get(ApsUseDynamicSensitivity)) { it.preferences.get(ApsDynIsfAdjustSensitivity) @@ -138,8 +138,8 @@ enum class BooleanKey( ApsAlwaysUseShortDeltas( "always_use_shortavg", false, - R.string.pref_title_aps_always_use_short_deltas, - R.string.pref_summary_aps_always_use_short_deltas, + KeysStrings.pref_title_aps_always_use_short_deltas, + KeysStrings.pref_summary_aps_always_use_short_deltas, defaultedBySM = true, hideParentScreenIfHidden = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -147,8 +147,8 @@ enum class BooleanKey( ApsDynIsfAdjustSensitivity( "dynisf_adjust_sensitivity", false, - R.string.pref_title_aps_dynisf_adjust_sensitivity, - R.string.pref_summary_aps_dynisf_adjust_sensitivity, + KeysStrings.pref_title_aps_dynisf_adjust_sensitivity, + KeysStrings.pref_summary_aps_dynisf_adjust_sensitivity, defaultedBySM = true, dependency = ApsUseDynamicSensitivity, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -156,79 +156,79 @@ enum class BooleanKey( ApsAmaAutosensAdjustTargets( "autosens_adjust_targets", true, - R.string.pref_title_aps_autosens_adjust_targets, - R.string.pref_summary_aps_autosens_adjust_targets, + KeysStrings.pref_title_aps_autosens_adjust_targets, + KeysStrings.pref_summary_aps_autosens_adjust_targets, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsAutoIsfHighTtRaisesSens( "high_temptarget_raises_sensitivity", false, - R.string.pref_title_aps_high_tt_raises_sensitivity, - R.string.pref_summary_aps_high_tt_raises_sensitivity, + KeysStrings.pref_title_aps_high_tt_raises_sensitivity, + KeysStrings.pref_summary_aps_high_tt_raises_sensitivity, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsAutoIsfLowTtLowersSens( "low_temptarget_lowers_sensitivity", false, - R.string.pref_title_aps_low_tt_lowers_sensitivity, - R.string.pref_summary_aps_low_tt_lowers_sensitivity, + KeysStrings.pref_title_aps_low_tt_lowers_sensitivity, + KeysStrings.pref_summary_aps_low_tt_lowers_sensitivity, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ApsUseAutoIsfWeights("openapsama_enable_autoISF", false, R.string.pref_title_aps_use_autoisf_weights, R.string.pref_summary_aps_use_autoisf_weights, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ApsUseAutoIsfWeights("openapsama_enable_autoISF", false, KeysStrings.pref_title_aps_use_autoisf_weights, KeysStrings.pref_summary_aps_use_autoisf_weights, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsAutoIsfSmbOnEvenTarget( "Enable alternative activation of SMB always", false, - R.string.pref_title_aps_smb_on_even_target, - R.string.pref_summary_aps_smb_on_even_target, + KeysStrings.pref_title_aps_smb_on_even_target, + KeysStrings.pref_summary_aps_smb_on_even_target, defaultedBySM = true, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - MaintenanceEnableFabric("enable_fabric2", true, R.string.pref_title_maintenance_enable_fabric, defaultedBySM = true, hideParentScreenIfHidden = true), + MaintenanceEnableFabric("enable_fabric2", true, KeysStrings.pref_title_maintenance_enable_fabric, defaultedBySM = true, hideParentScreenIfHidden = true), // Master-only (not a follower client): unattended settings export backs up the local config, which on a // client is derived from the master. showInNsClientMode=false hides it in apsMode + pumpControlMode only; // hideParentScreenIfHidden collapses the now-empty "Unattended Settings Export" subscreen on a client. - MaintenanceEnableExportSettingsAutomation("enable_unattended_export", false, R.string.pref_title_maintenance_enable_export_automation, defaultedBySM = false, showInNsClientMode = false, hideParentScreenIfHidden = true), + MaintenanceEnableExportSettingsAutomation("enable_unattended_export", false, KeysStrings.pref_title_maintenance_enable_export_automation, defaultedBySM = false, showInNsClientMode = false, hideParentScreenIfHidden = true), - AutotuneAutoSwitchProfile("autotune_auto", false, R.string.pref_title_autotune_auto_switch_profile, R.string.pref_summary_autotune_auto_switch_profile), - AutotuneCategorizeUamAsBasal("categorize_uam_as_basal", false, R.string.pref_title_autotune_categorize_uam_as_basal, R.string.pref_summary_autotune_categorize_uam_as_basal), - AutotuneTuneInsulinCurve("autotune_tune_insulin_curve", false, R.string.pref_title_autotune_tune_insulin_curve), - AutotuneCircadianIcIsf("autotune_circadian_ic_isf", false, R.string.pref_title_autotune_circadian_ic_isf, R.string.pref_summary_autotune_circadian_ic_isf), - AutotuneAdditionalLog("autotune_additional_log", false, R.string.pref_title_autotune_additional_log), + AutotuneAutoSwitchProfile("autotune_auto", false, KeysStrings.pref_title_autotune_auto_switch_profile, KeysStrings.pref_summary_autotune_auto_switch_profile), + AutotuneCategorizeUamAsBasal("categorize_uam_as_basal", false, KeysStrings.pref_title_autotune_categorize_uam_as_basal, KeysStrings.pref_summary_autotune_categorize_uam_as_basal), + AutotuneTuneInsulinCurve("autotune_tune_insulin_curve", false, KeysStrings.pref_title_autotune_tune_insulin_curve), + AutotuneCircadianIcIsf("autotune_circadian_ic_isf", false, KeysStrings.pref_title_autotune_circadian_ic_isf, KeysStrings.pref_summary_autotune_circadian_ic_isf), + AutotuneAdditionalLog("autotune_additional_log", false, KeysStrings.pref_title_autotune_additional_log), - SmsAllowRemoteCommands("smscommunicator_remotecommandsallowed", false, R.string.pref_title_sms_allow_remote_commands), - SmsReportPumpUnreachable("smscommunicator_report_pump_unreachable", true, R.string.pref_title_sms_report_pump_unreachable, R.string.pref_summary_sms_report_pump_unreachable), + SmsAllowRemoteCommands("smscommunicator_remotecommandsallowed", false, KeysStrings.pref_title_sms_allow_remote_commands), + SmsReportPumpUnreachable("smscommunicator_report_pump_unreachable", true, KeysStrings.pref_title_sms_report_pump_unreachable, KeysStrings.pref_summary_sms_report_pump_unreachable), - VirtualPumpStatusUpload("virtualpump_uploadstatus", false, R.string.pref_title_virtual_pump_status_upload, showInNsClientMode = false), - NsClientUploadData("ns_upload", true, R.string.pref_title_ns_upload_data, R.string.pref_summary_ns_upload_data, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptCgmData("ns_receive_cgm", false, R.string.pref_title_ns_receive_cgm, R.string.pref_summary_ns_receive_cgm, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptProfileStore("ns_receive_profile_store", false, R.string.pref_title_ns_receive_profile_store, R.string.pref_summary_ns_receive_profile_store, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptTempTarget("ns_receive_temp_target", false, R.string.pref_title_ns_receive_temp_target, R.string.pref_summary_ns_receive_temp_target, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptProfileSwitch("ns_receive_profile_switch", false, R.string.pref_title_ns_receive_profile_switch, R.string.pref_summary_ns_receive_profile_switch, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptInsulin("ns_receive_insulin", false, R.string.pref_title_ns_receive_insulin, R.string.pref_summary_ns_receive_insulin, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptCarbs("ns_receive_carbs", false, R.string.pref_title_ns_receive_carbs, R.string.pref_summary_ns_receive_carbs, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptTherapyEvent("ns_receive_therapy_events", false, R.string.pref_title_ns_receive_therapy_event, R.string.pref_summary_ns_receive_therapy_event, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptRunningMode("ns_receive_running_mode", false, R.string.pref_title_ns_receive_running_mode, R.string.pref_summary_ns_receive_running_mode, showInNsClientMode = false, hideParentScreenIfHidden = true), - NsClientAcceptTbrEb("ns_receive_tbr_eb", false, R.string.pref_title_ns_receive_tbr_eb, R.string.pref_summary_ns_receive_tbr_eb, showInNsClientMode = false, engineeringModeOnly = true), - NsClientNotificationsFromAlarms("ns_alarms", false, R.string.pref_title_ns_notifications_from_alarms, calculatedDefaultValue = true), - NsClientNotificationsFromAnnouncements("ns_announcements", false, R.string.pref_title_ns_notifications_from_announcements, calculatedDefaultValue = true), - NsClientUseCellular("ns_cellular", true, R.string.pref_title_ns_use_cellular), - NsClientUseRoaming("ns_allow_roaming", true, R.string.pref_title_ns_use_roaming, dependency = NsClientUseCellular), - NsClientUseWifi("ns_wifi", true, R.string.pref_title_ns_use_wifi), - NsClientUseOnBattery("ns_battery", true, R.string.pref_title_ns_use_on_battery), - NsClientUseOnCharging("ns_charging", true, R.string.pref_title_ns_use_on_charging), - NsClientLogAppStart("ns_log_app_started_event", false, R.string.pref_title_ns_log_app_start, calculatedDefaultValue = true), - NsClientCreateAnnouncementsFromErrors("ns_create_announcements_from_errors", false, R.string.pref_title_ns_create_announcements_from_errors, calculatedDefaultValue = true, showInNsClientMode = false), - NsClientCreateAnnouncementsFromCarbsReq("ns_create_announcements_from_carbs_req", false, R.string.pref_title_ns_create_announcements_from_carbs_req, calculatedDefaultValue = true, showInNsClientMode = false), - NsClientSlowSync("ns_sync_slow", false, R.string.pref_title_ns_slow_sync), - NsClient3UseWs("ns_use_ws", true, R.string.pref_title_ns_use_ws, R.string.pref_summary_ns_use_ws), + VirtualPumpStatusUpload("virtualpump_uploadstatus", false, KeysStrings.pref_title_virtual_pump_status_upload, showInNsClientMode = false), + NsClientUploadData("ns_upload", true, KeysStrings.pref_title_ns_upload_data, KeysStrings.pref_summary_ns_upload_data, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptCgmData("ns_receive_cgm", false, KeysStrings.pref_title_ns_receive_cgm, KeysStrings.pref_summary_ns_receive_cgm, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptProfileStore("ns_receive_profile_store", false, KeysStrings.pref_title_ns_receive_profile_store, KeysStrings.pref_summary_ns_receive_profile_store, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptTempTarget("ns_receive_temp_target", false, KeysStrings.pref_title_ns_receive_temp_target, KeysStrings.pref_summary_ns_receive_temp_target, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptProfileSwitch("ns_receive_profile_switch", false, KeysStrings.pref_title_ns_receive_profile_switch, KeysStrings.pref_summary_ns_receive_profile_switch, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptInsulin("ns_receive_insulin", false, KeysStrings.pref_title_ns_receive_insulin, KeysStrings.pref_summary_ns_receive_insulin, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptCarbs("ns_receive_carbs", false, KeysStrings.pref_title_ns_receive_carbs, KeysStrings.pref_summary_ns_receive_carbs, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptTherapyEvent("ns_receive_therapy_events", false, KeysStrings.pref_title_ns_receive_therapy_event, KeysStrings.pref_summary_ns_receive_therapy_event, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptRunningMode("ns_receive_running_mode", false, KeysStrings.pref_title_ns_receive_running_mode, KeysStrings.pref_summary_ns_receive_running_mode, showInNsClientMode = false, hideParentScreenIfHidden = true), + NsClientAcceptTbrEb("ns_receive_tbr_eb", false, KeysStrings.pref_title_ns_receive_tbr_eb, KeysStrings.pref_summary_ns_receive_tbr_eb, showInNsClientMode = false, engineeringModeOnly = true), + NsClientNotificationsFromAlarms("ns_alarms", false, KeysStrings.pref_title_ns_notifications_from_alarms, calculatedDefaultValue = true), + NsClientNotificationsFromAnnouncements("ns_announcements", false, KeysStrings.pref_title_ns_notifications_from_announcements, calculatedDefaultValue = true), + NsClientUseCellular("ns_cellular", true, KeysStrings.pref_title_ns_use_cellular), + NsClientUseRoaming("ns_allow_roaming", true, KeysStrings.pref_title_ns_use_roaming, dependency = NsClientUseCellular), + NsClientUseWifi("ns_wifi", true, KeysStrings.pref_title_ns_use_wifi), + NsClientUseOnBattery("ns_battery", true, KeysStrings.pref_title_ns_use_on_battery), + NsClientUseOnCharging("ns_charging", true, KeysStrings.pref_title_ns_use_on_charging), + NsClientLogAppStart("ns_log_app_started_event", false, KeysStrings.pref_title_ns_log_app_start, calculatedDefaultValue = true), + NsClientCreateAnnouncementsFromErrors("ns_create_announcements_from_errors", false, KeysStrings.pref_title_ns_create_announcements_from_errors, calculatedDefaultValue = true, showInNsClientMode = false), + NsClientCreateAnnouncementsFromCarbsReq("ns_create_announcements_from_carbs_req", false, KeysStrings.pref_title_ns_create_announcements_from_carbs_req, calculatedDefaultValue = true, showInNsClientMode = false), + NsClientSlowSync("ns_sync_slow", false, KeysStrings.pref_title_ns_slow_sync), + NsClient3UseWs("ns_use_ws", true, KeysStrings.pref_title_ns_use_ws, KeysStrings.pref_summary_ns_use_ws), NsClientAllowClientControl( "ns_allow_client_control", false, - R.string.pref_title_ns_allow_client_control, R.string.pref_summary_ns_allow_client_control, + KeysStrings.pref_title_ns_allow_client_control, KeysStrings.pref_summary_ns_allow_client_control, // The rich stop/allow-communication switch lives on the Authorized clients screen; it is ALSO exposed in a // "Remote control" category on the NSCv3 settings screen (NSClientV3Plugin.getPreferenceScreenContent) so it // is reachable from search. Default OFF, but ON in simple mode (resolved in PreferencesImpl.calculatedDefaultValue). Hidden on a client. @@ -241,26 +241,24 @@ enum class BooleanKey( // value for this key (see RunningConfigurationImpl), not the raw default. sync = SyncSpec(SyncChannel.Cold, SyncDirection.MasterOnly) ), - OpenHumansWifiOnly("oh_wifi_only", true, R.string.pref_title_openhumans_wifi_only), - OpenHumansChargingOnly("oh_charging_only", false, R.string.pref_title_openhumans_charging_only), - XdripSendStatus("xdrip_send_status", false, R.string.pref_title_xdrip_send_status), - XdripSendDetailedIob("xdripstatus_detailediob", true, R.string.pref_title_xdrip_send_detailed_iob, R.string.pref_summary_xdrip_send_detailed_iob, defaultedBySM = true, hideParentScreenIfHidden = true), - XdripSendBgi("xdripstatus_showbgi", true, R.string.pref_title_xdrip_send_bgi, R.string.pref_summary_xdrip_send_bgi, defaultedBySM = true, hideParentScreenIfHidden = true), - WearControl(key = "wearcontrol", defaultValue = false, titleResId = R.string.pref_title_wear_control, summaryResId = R.string.pref_summary_wear_control), - WearWizardBg(key = "wearwizard_bg", defaultValue = true, titleResId = R.string.pref_title_wear_wizard_bg, dependency = WearControl, hideParentScreenIfHidden = true), - WearWizardTt(key = "wearwizard_tt", defaultValue = false, titleResId = R.string.pref_title_wear_wizard_tt, dependency = WearControl, hideParentScreenIfHidden = true), - WearWizardTrend(key = "wearwizard_trend", defaultValue = false, titleResId = R.string.pref_title_wear_wizard_trend, dependency = WearControl, hideParentScreenIfHidden = true), - WearWizardCob(key = "wearwizard_cob", defaultValue = true, titleResId = R.string.pref_title_wear_wizard_cob, dependency = WearControl, hideParentScreenIfHidden = true), - WearWizardIob(key = "wearwizard_iob", defaultValue = true, titleResId = R.string.pref_title_wear_wizard_iob, dependency = WearControl, hideParentScreenIfHidden = true), - WearCustomWatchfaceAuthorization(key = "wear_custom_watchface_autorization", defaultValue = false, titleResId = R.string.pref_title_wear_custom_watchface_authorization), - WearNotifyOnSmb(key = "wear_notifySMB", defaultValue = true, titleResId = R.string.pref_title_wear_notify_on_smb, summaryResId = R.string.pref_summary_wear_notify_on_smb), - WearBroadcastData(key = "wear_broadcast_data", defaultValue = false, titleResId = R.string.pref_title_wear_broadcast_data, summaryResId = R.string.pref_summary_wear_broadcast_data, showInApsMode = false, showInPumpControlMode = false), + OpenHumansWifiOnly("oh_wifi_only", true, KeysStrings.pref_title_openhumans_wifi_only), + OpenHumansChargingOnly("oh_charging_only", false, KeysStrings.pref_title_openhumans_charging_only), + XdripSendStatus("xdrip_send_status", false, KeysStrings.pref_title_xdrip_send_status), + XdripSendDetailedIob("xdripstatus_detailediob", true, KeysStrings.pref_title_xdrip_send_detailed_iob, KeysStrings.pref_summary_xdrip_send_detailed_iob, defaultedBySM = true, hideParentScreenIfHidden = true), + XdripSendBgi("xdripstatus_showbgi", true, KeysStrings.pref_title_xdrip_send_bgi, KeysStrings.pref_summary_xdrip_send_bgi, defaultedBySM = true, hideParentScreenIfHidden = true), + WearControl(key = "wearcontrol", defaultValue = false, title = KeysStrings.pref_title_wear_control, summary = KeysStrings.pref_summary_wear_control), + WearWizardBg(key = "wearwizard_bg", defaultValue = true, title = KeysStrings.pref_title_wear_wizard_bg, dependency = WearControl, hideParentScreenIfHidden = true), + WearWizardTt(key = "wearwizard_tt", defaultValue = false, title = KeysStrings.pref_title_wear_wizard_tt, dependency = WearControl, hideParentScreenIfHidden = true), + WearWizardTrend(key = "wearwizard_trend", defaultValue = false, title = KeysStrings.pref_title_wear_wizard_trend, dependency = WearControl, hideParentScreenIfHidden = true), + WearWizardCob(key = "wearwizard_cob", defaultValue = true, title = KeysStrings.pref_title_wear_wizard_cob, dependency = WearControl, hideParentScreenIfHidden = true), + WearWizardIob(key = "wearwizard_iob", defaultValue = true, title = KeysStrings.pref_title_wear_wizard_iob, dependency = WearControl, hideParentScreenIfHidden = true), + WearCustomWatchfaceAuthorization(key = "wear_custom_watchface_autorization", defaultValue = false, title = KeysStrings.pref_title_wear_custom_watchface_authorization), + WearNotifyOnSmb(key = "wear_notifySMB", defaultValue = true, title = KeysStrings.pref_title_wear_notify_on_smb, summary = KeysStrings.pref_summary_wear_notify_on_smb), + WearBroadcastData(key = "wear_broadcast_data", defaultValue = false, title = KeysStrings.pref_title_wear_broadcast_data, summary = KeysStrings.pref_summary_wear_broadcast_data, showInApsMode = false, showInPumpControlMode = false), - SiteRotationManagePump("site_rotation_manage_pump", defaultValue = false, titleResId = R.string.pref_title_site_rotation_manage_pump, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), - SiteRotationManageCgm("site_rotation_manage_cgm", defaultValue = false, titleResId = R.string.pref_title_site_rotation_manage_cgm, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + SiteRotationManagePump("site_rotation_manage_pump", defaultValue = false, title = KeysStrings.pref_title_site_rotation_manage_pump, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + SiteRotationManageCgm("site_rotation_manage_cgm", defaultValue = false, title = KeysStrings.pref_title_site_rotation_manage_cgm, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ; - override val title: TextRef = TextRef.AndroidRes(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt index 45e7bb97fe36..b9f03e3f4114 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt @@ -12,8 +12,8 @@ enum class DoubleKey( override val defaultValue: Double, override val min: Double, override val max: Double, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val defaultedBySM: Boolean = false, override val calculatedBySM: Boolean = false, @@ -33,8 +33,8 @@ enum class DoubleKey( defaultValue = 0.5, min = -5.0, max = 5.0, - titleResId = R.string.pref_title_insulin_button_increment_1, - summaryResId = R.string.insulin_increment_button_message, + title = KeysStrings.pref_title_insulin_button_increment_1, + summary = KeysStrings.insulin_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowInsulinButton, unitType = UnitType.INSULIN, @@ -45,8 +45,8 @@ enum class DoubleKey( defaultValue = 1.0, min = -5.0, max = 5.0, - titleResId = R.string.pref_title_insulin_button_increment_2, - summaryResId = R.string.insulin_increment_button_message, + title = KeysStrings.pref_title_insulin_button_increment_2, + summary = KeysStrings.insulin_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowInsulinButton, unitType = UnitType.INSULIN, @@ -57,24 +57,24 @@ enum class DoubleKey( defaultValue = 2.0, min = -5.0, max = 5.0, - titleResId = R.string.pref_title_insulin_button_increment_3, - summaryResId = R.string.insulin_increment_button_message, + title = KeysStrings.pref_title_insulin_button_increment_3, + summary = KeysStrings.insulin_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowInsulinButton, unitType = UnitType.INSULIN, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - ActionsFillButton1(key = "fill_button1", defaultValue = 0.3, min = 0.05, max = 20.0, titleResId = R.string.pref_title_fill_button_1, defaultedBySM = true, hideParentScreenIfHidden = true, unitType = UnitType.INSULIN), - ActionsFillButton2(key = "fill_button2", defaultValue = 0.0, min = 0.0, max = 20.0, titleResId = R.string.pref_title_fill_button_2, defaultedBySM = true, unitType = UnitType.INSULIN), - ActionsFillButton3(key = "fill_button3", defaultValue = 0.0, min = 0.0, max = 20.0, titleResId = R.string.pref_title_fill_button_3, defaultedBySM = true, unitType = UnitType.INSULIN), - SafetyMaxBolus(key = "treatmentssafety_maxbolus", defaultValue = 3.0, min = 0.1, max = 60.0, titleResId = R.string.pref_title_max_bolus, unitType = UnitType.INSULIN, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + ActionsFillButton1(key = "fill_button1", defaultValue = 0.3, min = 0.05, max = 20.0, title = KeysStrings.pref_title_fill_button_1, defaultedBySM = true, hideParentScreenIfHidden = true, unitType = UnitType.INSULIN), + ActionsFillButton2(key = "fill_button2", defaultValue = 0.0, min = 0.0, max = 20.0, title = KeysStrings.pref_title_fill_button_2, defaultedBySM = true, unitType = UnitType.INSULIN), + ActionsFillButton3(key = "fill_button3", defaultValue = 0.0, min = 0.0, max = 20.0, title = KeysStrings.pref_title_fill_button_3, defaultedBySM = true, unitType = UnitType.INSULIN), + SafetyMaxBolus(key = "treatmentssafety_maxbolus", defaultValue = 3.0, min = 0.1, max = 60.0, title = KeysStrings.pref_title_max_bolus, unitType = UnitType.INSULIN, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsMaxBasal( key = "openapsma_max_basal", defaultValue = 1.0, min = 0.1, max = 25.0, - titleResId = R.string.pref_title_max_basal, - summaryResId = R.string.openapsma_max_basal_summary, + title = KeysStrings.pref_title_max_basal, + summary = KeysStrings.openapsma_max_basal_summary, defaultedBySM = true, calculatedBySM = true, unitType = UnitType.INSULIN_RATE, @@ -85,8 +85,8 @@ enum class DoubleKey( defaultValue = 3.0, min = 0.0, max = 70.0, - titleResId = R.string.pref_title_smb_max_iob, - summaryResId = R.string.openapssmb_max_iob_summary, + title = KeysStrings.pref_title_smb_max_iob, + summary = KeysStrings.openapssmb_max_iob_summary, defaultedBySM = true, calculatedBySM = true, unitType = UnitType.INSULIN, @@ -97,8 +97,8 @@ enum class DoubleKey( defaultValue = 1.5, min = 0.0, max = 25.0, - titleResId = R.string.pref_title_ama_max_iob, - summaryResId = R.string.openapsma_max_iob_summary, + title = KeysStrings.pref_title_ama_max_iob, + summary = KeysStrings.openapsma_max_iob_summary, defaultedBySM = true, calculatedBySM = true, unitType = UnitType.INSULIN, @@ -109,8 +109,8 @@ enum class DoubleKey( defaultValue = 3.0, min = 1.0, max = 10.0, - titleResId = R.string.pref_title_max_daily_multiplier, - summaryResId = R.string.openapsama_max_daily_safety_multiplier_summary, + title = KeysStrings.pref_title_max_daily_multiplier, + summary = KeysStrings.openapsama_max_daily_safety_multiplier_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -120,8 +120,8 @@ enum class DoubleKey( defaultValue = 4.0, min = 1.0, max = 10.0, - titleResId = R.string.pref_title_current_basal_multiplier, - summaryResId = R.string.openapsama_current_basal_safety_multiplier_summary, + title = KeysStrings.pref_title_current_basal_multiplier, + summary = KeysStrings.openapsama_current_basal_safety_multiplier_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -131,8 +131,8 @@ enum class DoubleKey( defaultValue = 2.0, min = 1.0, max = 10.0, - titleResId = R.string.pref_title_bolus_snooze_divisor, - summaryResId = R.string.openapsama_bolus_snooze_dia_divisor_summary, + title = KeysStrings.pref_title_bolus_snooze_divisor, + summary = KeysStrings.openapsama_bolus_snooze_dia_divisor_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -142,8 +142,8 @@ enum class DoubleKey( defaultValue = 3.0, min = 1.0, max = 12.0, - titleResId = R.string.pref_title_ama_min_5m_carbs_impact, - summaryResId = R.string.openapsama_min_5m_carb_impact_summary, + title = KeysStrings.pref_title_ama_min_5m_carbs_impact, + summary = KeysStrings.openapsama_min_5m_carb_impact_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -153,8 +153,8 @@ enum class DoubleKey( defaultValue = 8.0, min = 1.0, max = 12.0, - titleResId = R.string.pref_title_smb_min_5m_carbs_impact, - summaryResId = R.string.openapsama_min_5m_carb_impact_summary, + title = KeysStrings.pref_title_smb_min_5m_carbs_impact, + summary = KeysStrings.openapsama_min_5m_carb_impact_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -164,8 +164,8 @@ enum class DoubleKey( defaultValue = 6.0, min = 4.0, max = 10.0, - titleResId = R.string.pref_title_absorption_cutoff, - summaryResId = R.string.absorption_cutoff_summary, + title = KeysStrings.pref_title_absorption_cutoff, + summary = KeysStrings.absorption_cutoff_summary, unitType = UnitType.HOURS_DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), @@ -174,8 +174,8 @@ enum class DoubleKey( defaultValue = 6.0, min = 4.0, max = 10.0, - titleResId = R.string.pref_title_absorption_maxtime, - summaryResId = R.string.absorption_max_time_summary, + title = KeysStrings.pref_title_absorption_maxtime, + summary = KeysStrings.absorption_max_time_summary, unitType = UnitType.HOURS_DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), @@ -184,8 +184,8 @@ enum class DoubleKey( defaultValue = 0.7, min = 0.1, max = 1.0, - titleResId = R.string.pref_title_autosens_min, - summaryResId = R.string.openapsama_autosens_min_summary, + title = KeysStrings.pref_title_autosens_min, + summary = KeysStrings.openapsama_autosens_min_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -195,8 +195,8 @@ enum class DoubleKey( defaultValue = 1.2, min = 0.5, max = 3.0, - titleResId = R.string.pref_title_autosens_max, - summaryResId = R.string.openapsama_autosens_max_summary, + title = KeysStrings.pref_title_autosens_max, + summary = KeysStrings.openapsama_autosens_max_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -206,8 +206,8 @@ enum class DoubleKey( defaultValue = 1.0, min = 0.3, max = 1.0, - titleResId = R.string.pref_title_autoisf_min, - summaryResId = R.string.openapsama_autoISF_min_summary, + title = KeysStrings.pref_title_autoisf_min, + summary = KeysStrings.openapsama_autoISF_min_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -217,8 +217,8 @@ enum class DoubleKey( defaultValue = 1.0, min = 1.0, max = 3.0, - titleResId = R.string.pref_title_autoisf_max, - summaryResId = R.string.openapsama_autoISF_max_summary, + title = KeysStrings.pref_title_autoisf_max, + summary = KeysStrings.openapsama_autoISF_max_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -228,8 +228,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 1.0, - titleResId = R.string.pref_title_bg_accel_weight, - summaryResId = R.string.openapsama_bgAccel_ISF_weight_summary, + title = KeysStrings.pref_title_bg_accel_weight, + summary = KeysStrings.openapsama_bgAccel_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -239,8 +239,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 1.0, - titleResId = R.string.pref_title_bg_brake_weight, - summaryResId = R.string.openapsama_bgBrake_ISF_weight_summary, + title = KeysStrings.pref_title_bg_brake_weight, + summary = KeysStrings.openapsama_bgBrake_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -250,8 +250,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 2.0, - titleResId = R.string.pref_title_low_bg_weight, - summaryResId = R.string.openapsama_lower_ISFrange_weight_summary, + title = KeysStrings.pref_title_low_bg_weight, + summary = KeysStrings.openapsama_lower_ISFrange_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -261,8 +261,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 2.0, - titleResId = R.string.pref_title_high_bg_weight, - summaryResId = R.string.openapsama_higher_ISFrange_weight_summary, + title = KeysStrings.pref_title_high_bg_weight, + summary = KeysStrings.openapsama_higher_ISFrange_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -272,8 +272,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 100.0, - titleResId = R.string.pref_title_smb_delivery_ratio_bg_range, - summaryResId = R.string.openapsama_smb_delivery_ratio_bg_range_summary, + title = KeysStrings.pref_title_smb_delivery_ratio_bg_range, + summary = KeysStrings.openapsama_smb_delivery_ratio_bg_range_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -283,8 +283,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 0.15, - titleResId = R.string.pref_title_pp_weight, - summaryResId = R.string.openapsama_pp_ISF_weight_summary, + title = KeysStrings.pref_title_pp_weight, + summary = KeysStrings.openapsama_pp_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_3, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -294,8 +294,8 @@ enum class DoubleKey( defaultValue = 0.0, min = 0.0, max = 3.0, - titleResId = R.string.pref_title_dura_weight, - summaryResId = R.string.openapsama_dura_ISF_weight_summary, + title = KeysStrings.pref_title_dura_weight, + summary = KeysStrings.openapsama_dura_ISF_weight_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -305,8 +305,8 @@ enum class DoubleKey( defaultValue = 0.5, min = 0.1, max = 1.0, - titleResId = R.string.pref_title_smb_delivery_ratio, - summaryResId = R.string.openapsama_smb_delivery_ratio_summary, + title = KeysStrings.pref_title_smb_delivery_ratio, + summary = KeysStrings.openapsama_smb_delivery_ratio_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -316,8 +316,8 @@ enum class DoubleKey( defaultValue = 0.5, min = 0.1, max = 1.0, - titleResId = R.string.pref_title_smb_delivery_ratio_min, - summaryResId = R.string.openapsama_smb_delivery_ratio_min_summary, + title = KeysStrings.pref_title_smb_delivery_ratio_min, + summary = KeysStrings.openapsama_smb_delivery_ratio_min_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -327,8 +327,8 @@ enum class DoubleKey( defaultValue = 0.5, min = 0.5, max = 1.0, - titleResId = R.string.pref_title_smb_delivery_ratio_max, - summaryResId = R.string.openapsama_smb_delivery_ratio_max_summary, + title = KeysStrings.pref_title_smb_delivery_ratio_max, + summary = KeysStrings.openapsama_smb_delivery_ratio_max_summary, defaultedBySM = true, unitType = UnitType.DOUBLE_2, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -338,8 +338,8 @@ enum class DoubleKey( defaultValue = 1.0, min = 1.0, max = 5.0, - titleResId = R.string.pref_title_smb_max_range_extension, - summaryResId = R.string.openapsama_smb_max_range_extension_summary, + title = KeysStrings.pref_title_smb_max_range_extension, + summary = KeysStrings.openapsama_smb_max_range_extension_summary, defaultedBySM = true, unitType = UnitType.DOUBLE, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -347,6 +347,4 @@ enum class DoubleKey( ; - override val title: TextRef = TextRef.AndroidRes(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt index 9fa5e405a18a..d39ee5ece7a6 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt @@ -14,10 +14,10 @@ enum class IntKey( override val defaultValue: Int, override val min: Int, override val max: Int, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - private val entriesResIds: Map = emptyMap(), + private val entriesRefs: Map = emptyMap(), override val defaultedBySM: Boolean = false, override val calculatedDefaultValue: Boolean = false, override val showInApsMode: Boolean = true, @@ -39,8 +39,8 @@ enum class IntKey( defaultValue = 5, min = -50, max = 50, - titleResId = R.string.pref_title_carbs_button_increment_1, - summaryResId = R.string.carb_increment_button_message, + title = KeysStrings.pref_title_carbs_button_increment_1, + summary = KeysStrings.carb_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowCarbsButton, unitType = UnitType.GRAMS, @@ -51,8 +51,8 @@ enum class IntKey( defaultValue = 10, min = -50, max = 50, - titleResId = R.string.pref_title_carbs_button_increment_2, - summaryResId = R.string.carb_increment_button_message, + title = KeysStrings.pref_title_carbs_button_increment_2, + summary = KeysStrings.carb_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowCarbsButton, unitType = UnitType.GRAMS, @@ -63,8 +63,8 @@ enum class IntKey( defaultValue = 20, min = -50, max = 50, - titleResId = R.string.pref_title_carbs_button_increment_3, - summaryResId = R.string.carb_increment_button_message, + title = KeysStrings.pref_title_carbs_button_increment_3, + summary = KeysStrings.carb_increment_button_message, defaultedBySM = true, dependency = BooleanKey.OverviewShowCarbsButton, unitType = UnitType.GRAMS, @@ -76,7 +76,7 @@ enum class IntKey( defaultValue = 48, min = 24, max = 240, - titleResId = R.string.pref_title_cage_warning, + title = KeysStrings.pref_title_cage_warning, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -86,7 +86,7 @@ enum class IntKey( defaultValue = 72, min = 24, max = 240, - titleResId = R.string.pref_title_cage_critical, + title = KeysStrings.pref_title_cage_critical, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -96,7 +96,7 @@ enum class IntKey( defaultValue = 72, min = 24, max = 240, - titleResId = R.string.pref_title_iage_warning, + title = KeysStrings.pref_title_iage_warning, defaultedBySM = true, visibility = ElementVisibility.NON_PATCH_PUMP, unitType = UnitType.HOURS, @@ -107,7 +107,7 @@ enum class IntKey( defaultValue = 144, min = 24, max = 240, - titleResId = R.string.pref_title_iage_critical, + title = KeysStrings.pref_title_iage_critical, defaultedBySM = true, visibility = ElementVisibility.NON_PATCH_PUMP, unitType = UnitType.HOURS, @@ -118,7 +118,7 @@ enum class IntKey( defaultValue = 216, min = 24, max = 720, - titleResId = R.string.pref_title_sage_warning, + title = KeysStrings.pref_title_sage_warning, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -128,7 +128,7 @@ enum class IntKey( defaultValue = 240, min = 24, max = 720, - titleResId = R.string.pref_title_sage_critical, + title = KeysStrings.pref_title_sage_critical, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -138,7 +138,7 @@ enum class IntKey( defaultValue = 25, min = 0, max = 100, - titleResId = R.string.pref_title_sbat_warning, + title = KeysStrings.pref_title_sbat_warning, defaultedBySM = true, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -148,7 +148,7 @@ enum class IntKey( defaultValue = 5, min = 0, max = 100, - titleResId = R.string.pref_title_sbat_critical, + title = KeysStrings.pref_title_sbat_critical, defaultedBySM = true, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -158,7 +158,7 @@ enum class IntKey( defaultValue = 216, min = 24, max = 1000, - titleResId = R.string.pref_title_bage_warning, + title = KeysStrings.pref_title_bage_warning, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -168,7 +168,7 @@ enum class IntKey( defaultValue = 240, min = 24, max = 1000, - titleResId = R.string.pref_title_bage_critical, + title = KeysStrings.pref_title_bage_critical, defaultedBySM = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -178,7 +178,7 @@ enum class IntKey( defaultValue = 80, min = 0, max = 300, - titleResId = R.string.pref_title_res_warning, + title = KeysStrings.pref_title_res_warning, defaultedBySM = true, unitType = UnitType.INSULIN_INT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -188,7 +188,7 @@ enum class IntKey( defaultValue = 10, min = 0, max = 300, - titleResId = R.string.pref_title_res_critical, + title = KeysStrings.pref_title_res_critical, defaultedBySM = true, unitType = UnitType.INSULIN_INT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -198,7 +198,7 @@ enum class IntKey( defaultValue = 51, min = 0, max = 100, - titleResId = R.string.pref_title_batt_warning, + title = KeysStrings.pref_title_batt_warning, defaultedBySM = true, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -208,7 +208,7 @@ enum class IntKey( defaultValue = 26, min = 0, max = 100, - titleResId = R.string.pref_title_batt_critical, + title = KeysStrings.pref_title_batt_critical, defaultedBySM = true, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -218,8 +218,8 @@ enum class IntKey( defaultValue = 100, min = 10, max = 100, - titleResId = R.string.pref_title_bolus_percentage, - summaryResId = R.string.deliverpartofboluswizard, + title = KeysStrings.pref_title_bolus_percentage, + summary = KeysStrings.deliverpartofboluswizard, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), @@ -228,8 +228,8 @@ enum class IntKey( defaultValue = 16, min = 6, max = 120, - titleResId = R.string.pref_title_reset_bolus_percentage_time, - summaryResId = R.string.deliver_part_of_boluswizard_reset_time, + title = KeysStrings.pref_title_reset_bolus_percentage_time, + summary = KeysStrings.deliver_part_of_boluswizard_reset_time, defaultedBySM = true, engineeringModeOnly = true, unitType = UnitType.MIN, @@ -240,7 +240,7 @@ enum class IntKey( defaultValue = 1, min = 0, max = 180, - titleResId = R.string.pref_title_protection_timeout, + title = KeysStrings.pref_title_protection_timeout, defaultedBySM = true, unitType = UnitType.SEC, visibility = ElementVisibility.stringNotEmpty { StringKey.ProtectionMasterPassword } @@ -253,15 +253,15 @@ enum class IntKey( defaultValue = ProtectionType.NONE.ordinal, min = ProtectionType.NONE.ordinal, max = ProtectionType.CUSTOM_PIN.ordinal, - titleResId = R.string.pref_title_protection_type_application, - summaryResId = R.string.pref_summary_protection_type_application, + title = KeysStrings.pref_title_protection_type_application, + summary = KeysStrings.pref_summary_protection_type_application, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - ProtectionType.NONE.ordinal to R.string.noprotection, - ProtectionType.BIOMETRIC.ordinal to R.string.biometric, - ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, - ProtectionType.CUSTOM_PASSWORD.ordinal to R.string.custom_password, - ProtectionType.CUSTOM_PIN.ordinal to R.string.custom_pin + entriesRefs = mapOf( + ProtectionType.NONE.ordinal to KeysStrings.noprotection, + ProtectionType.BIOMETRIC.ordinal to KeysStrings.biometric, + ProtectionType.MASTER_PASSWORD.ordinal to KeysStrings.master_password, + ProtectionType.CUSTOM_PASSWORD.ordinal to KeysStrings.custom_password, + ProtectionType.CUSTOM_PIN.ordinal to KeysStrings.custom_pin ), visibility = ElementVisibility.stringNotEmpty { StringKey.ProtectionMasterPassword } ), @@ -270,15 +270,15 @@ enum class IntKey( defaultValue = ProtectionType.NONE.ordinal, min = ProtectionType.NONE.ordinal, max = ProtectionType.CUSTOM_PIN.ordinal, - titleResId = R.string.pref_title_protection_type_bolus, - summaryResId = R.string.pref_summary_protection_type_bolus, + title = KeysStrings.pref_title_protection_type_bolus, + summary = KeysStrings.pref_summary_protection_type_bolus, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - ProtectionType.NONE.ordinal to R.string.noprotection, - ProtectionType.BIOMETRIC.ordinal to R.string.biometric, - ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, - ProtectionType.CUSTOM_PASSWORD.ordinal to R.string.custom_password, - ProtectionType.CUSTOM_PIN.ordinal to R.string.custom_pin + entriesRefs = mapOf( + ProtectionType.NONE.ordinal to KeysStrings.noprotection, + ProtectionType.BIOMETRIC.ordinal to KeysStrings.biometric, + ProtectionType.MASTER_PASSWORD.ordinal to KeysStrings.master_password, + ProtectionType.CUSTOM_PASSWORD.ordinal to KeysStrings.custom_password, + ProtectionType.CUSTOM_PIN.ordinal to KeysStrings.custom_pin ), visibility = ElementVisibility.stringNotEmpty { StringKey.ProtectionMasterPassword }, enabledCondition = PreferenceEnabledCondition { ctx -> @@ -290,26 +290,26 @@ enum class IntKey( defaultValue = ProtectionType.NONE.ordinal, min = ProtectionType.NONE.ordinal, max = ProtectionType.CUSTOM_PIN.ordinal, - titleResId = R.string.pref_title_protection_type_settings, - summaryResId = R.string.pref_summary_protection_type_settings, + title = KeysStrings.pref_title_protection_type_settings, + summary = KeysStrings.pref_summary_protection_type_settings, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - ProtectionType.NONE.ordinal to R.string.noprotection, - ProtectionType.BIOMETRIC.ordinal to R.string.biometric, - ProtectionType.MASTER_PASSWORD.ordinal to R.string.master_password, - ProtectionType.CUSTOM_PASSWORD.ordinal to R.string.custom_password, - ProtectionType.CUSTOM_PIN.ordinal to R.string.custom_pin + entriesRefs = mapOf( + ProtectionType.NONE.ordinal to KeysStrings.noprotection, + ProtectionType.BIOMETRIC.ordinal to KeysStrings.biometric, + ProtectionType.MASTER_PASSWORD.ordinal to KeysStrings.master_password, + ProtectionType.CUSTOM_PASSWORD.ordinal to KeysStrings.custom_password, + ProtectionType.CUSTOM_PIN.ordinal to KeysStrings.custom_pin ), visibility = ElementVisibility.stringNotEmpty { StringKey.ProtectionMasterPassword } ), - SafetyMaxCarbs(key = "treatmentssafety_maxcarbs", defaultValue = 48, min = 1, max = 200, titleResId = R.string.pref_title_max_carbs, unitType = UnitType.GRAMS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + SafetyMaxCarbs(key = "treatmentssafety_maxcarbs", defaultValue = 48, min = 1, max = 200, title = KeysStrings.pref_title_max_carbs, unitType = UnitType.GRAMS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), LoopOpenModeMinChange( key = "loop_openmode_min_change", defaultValue = 30, min = 0, max = 50, - titleResId = R.string.pref_title_open_mode_min_change, - summaryResId = R.string.loop_open_mode_min_change_summary, + title = KeysStrings.pref_title_open_mode_min_change, + summary = KeysStrings.loop_open_mode_min_change_summary, defaultedBySM = true, unitType = UnitType.PERCENT ), @@ -318,7 +318,7 @@ enum class IntKey( defaultValue = 3, min = 1, max = 10, - titleResId = R.string.pref_title_smb_frequency, + title = KeysStrings.pref_title_smb_frequency, defaultedBySM = true, dependency = BooleanKey.ApsUseSmb, unitType = UnitType.MIN, @@ -329,14 +329,14 @@ enum class IntKey( defaultValue = 30, min = 15, max = 120, - titleResId = R.string.pref_title_smb_max_minutes, + title = KeysStrings.pref_title_smb_max_minutes, defaultedBySM = true, dependency = BooleanKey.ApsUseSmb, unitType = UnitType.MIN, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ApsUamMaxMinutesOfBasalToLimitSmb( - key = "uamsmbmaxminutes", defaultValue = 30, min = 15, max = 120, titleResId = R.string.pref_title_uam_smb_max_minutes, summaryResId = R.string.uam_smb_max_minutes, defaultedBySM = true, dependency = BooleanKey.ApsUseSmb, + key = "uamsmbmaxminutes", defaultValue = 30, min = 15, max = 120, title = KeysStrings.pref_title_uam_smb_max_minutes, summary = KeysStrings.uam_smb_max_minutes, defaultedBySM = true, dependency = BooleanKey.ApsUseSmb, visibility = ElementVisibility { it.preferences.get(BooleanKey.ApsUseUam) }, unitType = UnitType.MIN, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -346,8 +346,8 @@ enum class IntKey( defaultValue = 1, min = 1, max = 100, - titleResId = R.string.pref_title_carbs_request_threshold, - summaryResId = R.string.carbs_req_threshold_summary, + title = KeysStrings.pref_title_carbs_request_threshold, + summary = KeysStrings.carbs_req_threshold_summary, defaultedBySM = true, unitType = UnitType.GRAMS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -357,8 +357,8 @@ enum class IntKey( defaultValue = 160, min = 120, max = 200, - titleResId = R.string.pref_title_half_basal_exercise_target, - summaryResId = R.string.half_basal_exercise_target_summary, + title = KeysStrings.pref_title_half_basal_exercise_target, + summary = KeysStrings.half_basal_exercise_target_summary, defaultedBySM = true, unitType = UnitType.MGDL, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -368,8 +368,8 @@ enum class IntKey( defaultValue = 100, min = 10, max = 100, - titleResId = R.string.pref_title_iob_threshold_percent, - summaryResId = R.string.openapsama_iob_threshold_percent_summary, + title = KeysStrings.pref_title_iob_threshold_percent, + summary = KeysStrings.openapsama_iob_threshold_percent_summary, defaultedBySM = true, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -379,8 +379,8 @@ enum class IntKey( defaultValue = 100, min = 1, max = 300, - titleResId = R.string.pref_title_dynisf_adjustment_factor, - summaryResId = R.string.dyn_isf_adjust_summary, + title = KeysStrings.pref_title_dynisf_adjustment_factor, + summary = KeysStrings.dyn_isf_adjust_summary, dependency = BooleanKey.ApsUseDynamicSensitivity, unitType = UnitType.PERCENT, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) @@ -390,19 +390,19 @@ enum class IntKey( defaultValue = 24, min = 4, max = 24, - titleResId = R.string.pref_title_autosens_period, - summaryResId = R.string.openapsama_autosens_period_summary, + title = KeysStrings.pref_title_autosens_period, + summary = KeysStrings.openapsama_autosens_period_summary, calculatedDefaultValue = true, unitType = UnitType.HOURS, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), - MaintenanceLogsAmount(key = "maintenance_logs_amount", defaultValue = 2, min = 1, max = 10, titleResId = R.string.pref_title_logs_amount, defaultedBySM = true), + MaintenanceLogsAmount(key = "maintenance_logs_amount", defaultValue = 2, min = 1, max = 10, title = KeysStrings.pref_title_logs_amount, defaultedBySM = true), AlertsStaleDataThreshold( key = "missed_bg_readings_threshold", defaultValue = 30, min = 15, max = 10000, - titleResId = R.string.pref_title_stale_data_threshold, + title = KeysStrings.pref_title_stale_data_threshold, defaultedBySM = true, dependency = BooleanKey.AlertMissedBgReading, unitType = UnitType.MIN @@ -412,20 +412,20 @@ enum class IntKey( defaultValue = 30, min = 30, max = 300, - titleResId = R.string.pref_title_pump_unreachable_threshold, + title = KeysStrings.pref_title_pump_unreachable_threshold, defaultedBySM = true, dependency = BooleanKey.AlertPumpUnreachable, unitType = UnitType.MIN ), - AutotuneDefaultTuneDays(key = "autotune_default_tune_days", defaultValue = 5, min = 1, max = 30, titleResId = R.string.pref_title_autotune_days, summaryResId = R.string.autotune_default_tune_days_summary, unitType = UnitType.DAYS), + AutotuneDefaultTuneDays(key = "autotune_default_tune_days", defaultValue = 5, min = 1, max = 30, title = KeysStrings.pref_title_autotune_days, summary = KeysStrings.autotune_default_tune_days_summary, unitType = UnitType.DAYS), SmsRemoteBolusDistance( key = "smscommunicator_remotebolusmindistance", defaultValue = 15, min = 3, max = 60, - titleResId = R.string.pref_title_sms_remote_bolus_distance, + title = KeysStrings.pref_title_sms_remote_bolus_distance, unitType = UnitType.MIN, // Enabled only when multiple phone numbers are configured (2FA requirement) enabledCondition = PreferenceEnabledCondition { ctx -> @@ -434,27 +434,25 @@ enum class IntKey( } ), - BgSourceRandomInterval(key = "randombg_interval_min", defaultValue = 5, min = 1, max = 15, titleResId = R.string.pref_title_random_bg_interval, defaultedBySM = true, unitType = UnitType.MIN), - NsClientAlarmStaleData(key = "ns_alarm_stale_data_value", defaultValue = 16, min = 15, max = 120, titleResId = R.string.pref_title_alarm_stale_data, unitType = UnitType.MIN), - NsClientUrgentAlarmStaleData(key = "ns_alarm_urgent_stale_data_value", defaultValue = 31, min = 30, max = 180, titleResId = R.string.pref_title_urgent_alarm_stale_data, unitType = UnitType.MIN), + BgSourceRandomInterval(key = "randombg_interval_min", defaultValue = 5, min = 1, max = 15, title = KeysStrings.pref_title_random_bg_interval, defaultedBySM = true, unitType = UnitType.MIN), + NsClientAlarmStaleData(key = "ns_alarm_stale_data_value", defaultValue = 16, min = 15, max = 120, title = KeysStrings.pref_title_alarm_stale_data, unitType = UnitType.MIN), + NsClientUrgentAlarmStaleData(key = "ns_alarm_urgent_stale_data_value", defaultValue = 31, min = 30, max = 180, title = KeysStrings.pref_title_urgent_alarm_stale_data, unitType = UnitType.MIN), SiteRotationUserProfile( key = "site_rotation_user_profile", defaultValue = 0, min = 0, max = 2, - titleResId = R.string.pref_title_site_rotation_profile, + title = KeysStrings.pref_title_site_rotation_profile, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - 0 to R.string.site_rotation_profile_man, - 1 to R.string.site_rotation_profile_woman, - 2 to R.string.site_rotation_profile_child + entriesRefs = mapOf( + 0 to KeysStrings.site_rotation_profile_man, + 1 to KeysStrings.site_rotation_profile_woman, + 2 to KeysStrings.site_rotation_profile_child ), sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), ; - override val title: TextRef = TextRef.AndroidRes(titleResId) - override val entries: Map = entriesResIds.mapValues { TextRef.AndroidRes(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } + override val entries: Map = entriesRefs } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt index e947b1ef2c37..77ab5552897a 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt @@ -16,8 +16,8 @@ import app.aaps.core.keys.interfaces.TextRef */ enum class IntentKey( override val key: String, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.CLICK, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -33,6 +33,4 @@ enum class IntentKey( // properties. The `;` is what separates the - empty - constant list from the members. ; - override val title: TextRef = TextRef.AndroidRes(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt index 6f89516ef154..9d1dbc677e14 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt @@ -13,10 +13,10 @@ import app.aaps.core.keys.interfaces.TextRef enum class StringKey( override val key: String, override val defaultValue: String, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, - private val entriesResIds: Map = emptyMap(), + private val entriesRefs: Map = emptyMap(), /** * Entry labels that are not a resource in this module. Only the units preference needs this: * "mg/dL" and "mmol/L" read the same in every language, and the `units_*` strings themselves @@ -43,7 +43,7 @@ enum class StringKey( GeneralUnits( key = "units", defaultValue = "mg/dl", - titleResId = R.string.pref_title_units, + title = KeysStrings.pref_title_units, preferenceType = PreferenceType.LIST, entriesLiterals = mapOf( "mg/dl" to "mg/dL", @@ -54,105 +54,105 @@ enum class StringKey( GeneralLanguage( key = "language", defaultValue = "default", - titleResId = R.string.pref_title_language, + title = KeysStrings.pref_title_language, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - "default" to R.string.lang_default, - "en" to R.string.lang_en, - "af" to R.string.lang_af, - "bg" to R.string.lang_bg, - "cs" to R.string.lang_cs, - "de" to R.string.lang_de, - "dk" to R.string.lang_dk, - "fr" to R.string.lang_fr, - "nl" to R.string.lang_nl, - "es" to R.string.lang_es, - "el" to R.string.lang_el, - "ga" to R.string.lang_ga, - "it" to R.string.lang_it, - "ko" to R.string.lang_ko, - "lt" to R.string.lang_lt, - "nb" to R.string.lang_nb, - "pl" to R.string.lang_pl, - "pt" to R.string.lang_pt, - "pt_BR" to R.string.lang_pt_br, - "ro" to R.string.lang_ro, - "ru" to R.string.lang_ru, - "sk" to R.string.lang_sk, - "sv" to R.string.lang_sv, - "tr" to R.string.lang_tr, - "zh_TW" to R.string.lang_zh_tw, - "zh_CN" to R.string.lang_zh_cn + entriesRefs = mapOf( + "default" to KeysStrings.lang_default, + "en" to KeysStrings.lang_en, + "af" to KeysStrings.lang_af, + "bg" to KeysStrings.lang_bg, + "cs" to KeysStrings.lang_cs, + "de" to KeysStrings.lang_de, + "dk" to KeysStrings.lang_dk, + "fr" to KeysStrings.lang_fr, + "nl" to KeysStrings.lang_nl, + "es" to KeysStrings.lang_es, + "el" to KeysStrings.lang_el, + "ga" to KeysStrings.lang_ga, + "it" to KeysStrings.lang_it, + "ko" to KeysStrings.lang_ko, + "lt" to KeysStrings.lang_lt, + "nb" to KeysStrings.lang_nb, + "pl" to KeysStrings.lang_pl, + "pt" to KeysStrings.lang_pt, + "pt_BR" to KeysStrings.lang_pt_br, + "ro" to KeysStrings.lang_ro, + "ru" to KeysStrings.lang_ru, + "sk" to KeysStrings.lang_sk, + "sv" to KeysStrings.lang_sv, + "tr" to KeysStrings.lang_tr, + "zh_TW" to KeysStrings.lang_zh_tw, + "zh_CN" to KeysStrings.lang_zh_cn ), defaultedBySM = true ), GeneralPatientName( key = "patient_name", defaultValue = "", - titleResId = R.string.pref_title_patient_name, - summaryResId = R.string.pref_summary_patient_name, + title = KeysStrings.pref_title_patient_name, + summary = KeysStrings.pref_summary_patient_name, validator = StringValidator.personName() ), GeneralDarkMode( key = "use_dark_mode", defaultValue = "dark", - titleResId = R.string.pref_title_app_color_scheme, - summaryResId = R.string.pref_summary_theme_switcher, + title = KeysStrings.pref_title_app_color_scheme, + summary = KeysStrings.pref_summary_theme_switcher, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - "dark" to R.string.pref_dark_theme, - "light" to R.string.pref_light_theme, - "system" to R.string.pref_follow_system_theme + entriesRefs = mapOf( + "dark" to KeysStrings.pref_dark_theme, + "light" to KeysStrings.pref_light_theme, + "system" to KeysStrings.pref_follow_system_theme ), defaultedBySM = true ), - AapsDirectoryUri(key = "aaps_directory", defaultValue = "", titleResId = R.string.pref_title_aaps_directory), + AapsDirectoryUri(key = "aaps_directory", defaultValue = "", title = KeysStrings.pref_title_aaps_directory), - ProtectionMasterPassword(key = "master_password", defaultValue = "", titleResId = R.string.pref_title_master_password, isPassword = true, isHashed = true), + ProtectionMasterPassword(key = "master_password", defaultValue = "", title = KeysStrings.pref_title_master_password, isPassword = true, isHashed = true), ProtectionSettingsPassword( - key = "settings_password", defaultValue = "", titleResId = R.string.pref_title_settings_password, isPassword = true, isHashed = true, + key = "settings_password", defaultValue = "", title = KeysStrings.pref_title_settings_password, isPassword = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeSettings }, ProtectionType.CUSTOM_PASSWORD.ordinal) ), ProtectionSettingsPin( - key = "settings_pin", defaultValue = "", titleResId = R.string.pref_title_settings_pin, isPin = true, isHashed = true, + key = "settings_pin", defaultValue = "", title = KeysStrings.pref_title_settings_pin, isPin = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeSettings }, ProtectionType.CUSTOM_PIN.ordinal) ), ProtectionApplicationPassword( - key = "application_password", defaultValue = "", titleResId = R.string.pref_title_application_password, isPassword = true, isHashed = true, + key = "application_password", defaultValue = "", title = KeysStrings.pref_title_application_password, isPassword = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeApplication }, ProtectionType.CUSTOM_PASSWORD.ordinal) ), ProtectionApplicationPin( - key = "application_pin", defaultValue = "", titleResId = R.string.pref_title_application_pin, isPin = true, isHashed = true, + key = "application_pin", defaultValue = "", title = KeysStrings.pref_title_application_pin, isPin = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeApplication }, ProtectionType.CUSTOM_PIN.ordinal) ), ProtectionBolusPassword( - key = "bolus_password", defaultValue = "", titleResId = R.string.pref_title_bolus_password, isPassword = true, isHashed = true, + key = "bolus_password", defaultValue = "", title = KeysStrings.pref_title_bolus_password, isPassword = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeBolus }, ProtectionType.CUSTOM_PASSWORD.ordinal) ), ProtectionBolusPin( - key = "bolus_pin", defaultValue = "", titleResId = R.string.pref_title_bolus_pin, isPin = true, isHashed = true, + key = "bolus_pin", defaultValue = "", title = KeysStrings.pref_title_bolus_pin, isPin = true, isHashed = true, visibility = ElementVisibility.intEquals({ IntKey.ProtectionTypeBolus }, ProtectionType.CUSTOM_PIN.ordinal) ), - SafetyAge(key = "age", defaultValue = "adult", titleResId = R.string.pref_title_patient_age, preferenceType = PreferenceType.LIST, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + SafetyAge(key = "age", defaultValue = "adult", title = KeysStrings.pref_title_patient_age, preferenceType = PreferenceType.LIST, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), MaintenanceEmail( key = "maintenance_logs_email", defaultValue = "logs@aaps.app", - titleResId = R.string.maintenance_email, + title = KeysStrings.maintenance_email, defaultedBySM = true, validator = StringValidator.email() ), - MaintenanceIdentification(key = "email_for_crash_report", defaultValue = "", titleResId = R.string.pref_title_identification), + MaintenanceIdentification(key = "email_for_crash_report", defaultValue = "", title = KeysStrings.pref_title_identification), AutomationLocation( key = "location", defaultValue = "PASSIVE", - titleResId = R.string.pref_title_automation_location, + title = KeysStrings.pref_title_automation_location, preferenceType = PreferenceType.LIST, - entriesResIds = mapOf( - "PASSIVE" to R.string.automation_location_passive, - "NETWORK" to R.string.automation_location_network, - "GPS" to R.string.automation_location_gps + entriesRefs = mapOf( + "PASSIVE" to KeysStrings.automation_location_passive, + "NETWORK" to KeysStrings.automation_location_network, + "GPS" to KeysStrings.automation_location_gps ), sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ), @@ -160,57 +160,55 @@ enum class StringKey( SmsAllowedNumbers( key = "smscommunicator_allowednumbers", defaultValue = "", - titleResId = R.string.smscommunicator_allowednumbers, - summaryResId = R.string.smscommunicator_allowednumbers_summary, + title = KeysStrings.smscommunicator_allowednumbers, + summary = KeysStrings.smscommunicator_allowednumbers_summary, validator = StringValidator.multiPhone() ), SmsOtpPassword( key = "smscommunicator_otp_password", defaultValue = "", - titleResId = R.string.smscommunicator_otp_pin, - summaryResId = R.string.smscommunicator_otp_pin_summary, + title = KeysStrings.smscommunicator_otp_pin, + summary = KeysStrings.smscommunicator_otp_pin_summary, dependency = BooleanKey.SmsAllowRemoteCommands, isPassword = true, validator = StringValidator.pinStrength() ), - VirtualPumpType(key = "virtualpump_type", defaultValue = "Generic AAPS", titleResId = R.string.pref_title_virtual_pump_type, preferenceType = PreferenceType.LIST), + VirtualPumpType(key = "virtualpump_type", defaultValue = "Generic AAPS", title = KeysStrings.pref_title_virtual_pump_type, preferenceType = PreferenceType.LIST), NsClientUrl( key = "nsclientinternal_url", defaultValue = "", - titleResId = R.string.ns_client_url_title, - summaryResId = R.string.ns_client_url_summary, + title = KeysStrings.ns_client_url_title, + summary = KeysStrings.ns_client_url_summary, validator = StringValidator.httpsUrl() ), NsClientApiSecret( key = "nsclientinternal_api_secret", defaultValue = "", - titleResId = R.string.ns_client_secret_title, - summaryResId = R.string.ns_client_secret_summary, + title = KeysStrings.ns_client_secret_title, + summary = KeysStrings.ns_client_secret_summary, isPassword = true, validator = StringValidator.minLength(12) ), NsClientWifiSsids( key = "ns_wifi_ssids", defaultValue = "", - titleResId = R.string.ns_wifi_ssids, - summaryResId = R.string.ns_wifi_ssids_summary, + title = KeysStrings.ns_wifi_ssids, + summary = KeysStrings.ns_wifi_ssids_summary, dependency = BooleanKey.NsClientUseWifi ), NsClientAccessToken( key = "nsclient_token", defaultValue = "", - titleResId = R.string.nsclient_token_title, - summaryResId = R.string.nsclient_token_summary, + title = KeysStrings.nsclient_token_title, + summary = KeysStrings.nsclient_token_summary, isPassword = true, validator = StringValidator.minLength(17) ), ; - override val title: TextRef = TextRef.AndroidRes(titleResId) override val entries: Map = - entriesResIds.mapValues { TextRef.AndroidRes(it.value) } + entriesLiterals.mapValues { TextRef.Literal(it.value) } - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } + entriesRefs + entriesLiterals.mapValues { TextRef.Literal(it.value) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt index a2e6d5d27a15..82a7795d552e 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt @@ -12,8 +12,8 @@ enum class UnitDoubleKey( override val defaultValue: Double, override val minMgdl: Int, override val maxMgdl: Int, - private val titleResId: Int, - private val summaryResId: Int? = null, + override val title: TextRef, + override val summary: TextRef? = null, override val preferenceType: PreferenceType = PreferenceType.TEXT_FIELD, override val defaultedBySM: Boolean = false, override val showInApsMode: Boolean = true, @@ -26,21 +26,19 @@ enum class UnitDoubleKey( override val sync: SyncSpec? = null ) : UnitDoublePreferenceKey { - OverviewLowMark(key = "low_mark", defaultValue = 72.0, minMgdl = 25, maxMgdl = 160, titleResId = R.string.pref_title_low_mark, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), - OverviewHighMark(key = "high_mark", defaultValue = 180.0, minMgdl = 90, maxMgdl = 250, titleResId = R.string.pref_title_high_mark, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + OverviewLowMark(key = "low_mark", defaultValue = 72.0, minMgdl = 25, maxMgdl = 160, title = KeysStrings.pref_title_low_mark, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), + OverviewHighMark(key = "high_mark", defaultValue = 180.0, minMgdl = 90, maxMgdl = 250, title = KeysStrings.pref_title_high_mark, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional)), ApsLgsThreshold( key = "lgsThreshold", defaultValue = 65.0, minMgdl = 60, maxMgdl = 100, - titleResId = R.string.pref_title_lgs_threshold, - summaryResId = R.string.lgs_threshold_summary, + title = KeysStrings.pref_title_lgs_threshold, + summary = KeysStrings.lgs_threshold_summary, defaultedBySM = true, dependency = BooleanKey.ApsUseDynamicSensitivity, sync = SyncSpec(SyncChannel.Cold, SyncDirection.Bidirectional) ) ; - override val title: TextRef = TextRef.AndroidRes(titleResId) - override val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } } diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt index 54de5728b019..8a2fd12a3c63 100644 --- a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt +++ b/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt @@ -15,12 +15,16 @@ package app.aaps.core.keys.interfaces * crash report as a number, because the same number means different things on different builds. * Persist the preference `key` instead, which is a stable string. * - * ### Why an Int and not a string name + * ### Names and ids * - * Resolving by name needs `Resources.getIdentifier()`, which is a reflective lookup that R8 cannot - * see - it would keep every string alive and silently return 0 for a typo. Keeping the Android - * resource id means the existing `R.string.x` references stay compile checked exactly as they are - * today. + * [Named] is for a module that has stopped naming Android resource ids in its own code. [AndroidRes] + * is for everything else, and there is a lot of it, so both stay. + * + * An earlier version of this note argued that a name could not work, because resolving one needs + * `Resources.getIdentifier()` - a reflective lookup R8 cannot see, which would keep every string + * alive and silently return 0 for a typo. That is true of a hand written name and false of a + * generated one: `GenerateKeyStringsTask` emits the names and their id map together from the same + * `strings.xml`, so R8 sees ordinary `R.string.x` references and a typo does not compile. */ sealed interface TextRef { @@ -37,6 +41,21 @@ sealed interface TextRef { */ data class AndroidRes(val id: Int, val args: List = emptyList()) : TextRef + /** + * A string named rather than numbered, so the declaring code holds nothing Android specific. + * + * [name] is the `name` attribute from `strings.xml`. Take these from the generated object for + * the owning module - `KeysStrings` for `:core:keys` - never by writing the string out by hand, + * because only the generated form is checked against the XML at build time. + * + * Each platform resolves the name its own way. Android looks it up in the generated id map and + * then reads it through `Resources`, so locale matching and the always English lookup work + * exactly as they did when keys carried ids. + * + * [args] behave as in [AndroidRes]. + */ + data class Named(val name: String, val args: List = emptyList()) : TextRef + /** * Text that is only known at run time - a scanned pump name, a wiki page title, a user label. * diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt index 95b8c19745af..9fdfe59a94c9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -2,24 +2,35 @@ package app.aaps.core.ui.compose import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef /** * Resolves a [TextRef] to text inside a Composable. * - * Every preference screen funnels through this one function, which is the point: when a module later - * moves its strings to `commonMain/composeResources`, only this resolver learns about the new - * negative-token form of [TextRef.AndroidRes]. The ~18 call sites do not change again. + * Every preference screen funnels through this one function, which is the point: a module that + * changes how it stores its strings changes only this resolver. The ~18 call sites do not change + * again. * - * On Android today [TextRef.AndroidRes.id] is always an ordinary `R.string` id, so this is a direct call - * through to the platform. + * Both resource forms end up in the platform `stringResource` on Android. [TextRef.AndroidRes] + * carries the id already; [TextRef.Named] carries the name from `strings.xml` and is turned into an + * id first, so Android keeps doing its own locale matching in both cases. */ @Composable fun stringResource(ref: TextRef): String = when (ref) { - is TextRef.Literal -> ref.text - is TextRef.AndroidRes -> + is TextRef.Literal -> ref.text + is TextRef.AndroidRes -> if (ref.args.isEmpty()) stringResource(ref.id) else stringResource(ref.id, *ref.args.toTypedArray()) + + is TextRef.Named -> { + val id = KeysStringIds.idOf(ref.name) + when { + id == null -> ref.name + ref.args.isEmpty() -> stringResource(id) + else -> stringResource(id, *ref.args.toTypedArray()) + } + } } /** Same, for an optional reference - returns null so callers can keep using `?.let { }`. */ From eeddd8182cbfdabb7eecf392d6bab35351c8203a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 17:17:45 +0200 Subject: [PATCH 019/146] :core:keys kmp --- core/keys/build.gradle.kts | 114 ++++++++++++------ .../core/keys/interfaces/ComposedKeyTest.kt | 0 .../res/values-ar-rSA/strings.xml | 0 .../res/values-bg-rBG/strings.xml | 0 .../res/values-ca-rES/strings.xml | 0 .../res/values-cs-rCZ/strings.xml | 0 .../res/values-da-rDK/strings.xml | 0 .../res/values-de-rDE/strings.xml | 0 .../res/values-el-rGR/strings.xml | 0 .../res/values-es-rES/strings.xml | 0 .../res/values-fr-rFR/strings.xml | 0 .../res/values-hr-rHR/strings.xml | 0 .../res/values-hu-rHU/strings.xml | 0 .../res/values-it-rIT/strings.xml | 0 .../res/values-iw-rIL/strings.xml | 0 .../res/values-ko-rKR/strings.xml | 0 .../res/values-lt-rLT/strings.xml | 0 .../res/values-nb-rNO/strings.xml | 0 .../res/values-nl-rNL/strings.xml | 0 .../res/values-pl-rPL/strings.xml | 0 .../res/values-pt-rBR/strings.xml | 0 .../res/values-pt-rPT/strings.xml | 0 .../res/values-ro-rRO/strings.xml | 0 .../res/values-ru-rRU/strings.xml | 0 .../res/values-sk-rSK/strings.xml | 0 .../res/values-sr-rCS/strings.xml | 0 .../res/values-sv-rSE/strings.xml | 0 .../res/values-tr-rTR/strings.xml | 0 .../res/values-uk-rUA/strings.xml | 0 .../res/values-vi-rVN/strings.xml | 0 .../res/values-zh-rCN/strings.xml | 0 .../res/values-zh-rTW/strings.xml | 0 .../res/values/strings.xml | 0 .../app/aaps/core/keys/BooleanComposedKey.kt | 0 .../kotlin/app/aaps/core/keys/BooleanKey.kt | 0 .../app/aaps/core/keys/BooleanNonKey.kt | 0 .../kotlin/app/aaps/core/keys/DoubleKey.kt | 0 .../kotlin/app/aaps/core/keys/DoubleNonKey.kt | 0 .../app/aaps/core/keys/IntComposedKey.kt | 0 .../kotlin/app/aaps/core/keys/IntKey.kt | 0 .../kotlin/app/aaps/core/keys/IntNonKey.kt | 0 .../kotlin/app/aaps/core/keys/IntentKey.kt | 0 .../app/aaps/core/keys/LongComposedKey.kt | 0 .../kotlin/app/aaps/core/keys/LongNonKey.kt | 0 .../app/aaps/core/keys/PreferenceType.kt | 0 .../core/keys/ProfileComposedBooleanKey.kt | 0 .../core/keys/ProfileComposedStringKey.kt | 0 .../app/aaps/core/keys/ProfileIntKey.kt | 0 .../app/aaps/core/keys/ProtectionType.kt | 0 .../kotlin/app/aaps/core/keys/StringKey.kt | 0 .../kotlin/app/aaps/core/keys/StringNonKey.kt | 0 .../app/aaps/core/keys/UnitDoubleKey.kt | 0 .../kotlin/app/aaps/core/keys/UnitType.kt | 0 .../BooleanComposedNonPreferenceKey.kt | 0 .../interfaces/BooleanNonPreferenceKey.kt | 0 .../keys/interfaces/BooleanPreferenceKey.kt | 0 .../aaps/core/keys/interfaces/ComposedKey.kt | 0 .../DoubleComposedNonPreferenceKey.kt | 0 .../keys/interfaces/DoubleNonPreferenceKey.kt | 0 .../keys/interfaces/DoublePreferenceKey.kt | 0 .../core/keys/interfaces/ElementVisibility.kt | 0 .../interfaces/IntComposedNonPreferenceKey.kt | 0 .../keys/interfaces/IntNonPreferenceKey.kt | 0 .../core/keys/interfaces/IntPreferenceKey.kt | 0 .../keys/interfaces/IntentPreferenceKey.kt | 0 .../LongComposedNonPreferenceKey.kt | 0 .../keys/interfaces/LongNonPreferenceKey.kt | 0 .../core/keys/interfaces/LongPreferenceKey.kt | 0 .../core/keys/interfaces/NonPreferenceKey.kt | 0 .../interfaces/PreferenceEnabledCondition.kt | 0 .../core/keys/interfaces/PreferenceKey.kt | 0 .../aaps/core/keys/interfaces/Preferences.kt | 0 .../StringComposedNonPreferenceKey.kt | 0 .../keys/interfaces/StringNonPreferenceKey.kt | 0 .../keys/interfaces/StringPreferenceKey.kt | 0 .../core/keys/interfaces/StringValidator.kt | 0 .../app/aaps/core/keys/interfaces/SyncSpec.kt | 0 .../app/aaps/core/keys/interfaces/TextRef.kt | 0 .../interfaces/UnitDoublePreferenceKey.kt | 0 .../core/keys/interfaces/VisibilityContext.kt | 0 gradle/libs.versions.toml | 4 + runtests.bat | 4 +- runtests.sh | 4 +- 83 files changed, 87 insertions(+), 39 deletions(-) rename core/keys/src/{test => androidHostTest}/kotlin/app/aaps/core/keys/interfaces/ComposedKeyTest.kt (100%) rename core/keys/src/{main => androidMain}/res/values-ar-rSA/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-bg-rBG/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-ca-rES/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-cs-rCZ/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-da-rDK/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-de-rDE/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-el-rGR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-es-rES/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-fr-rFR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-hr-rHR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-hu-rHU/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-it-rIT/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-iw-rIL/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-ko-rKR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-lt-rLT/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-nb-rNO/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-nl-rNL/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-pl-rPL/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-pt-rBR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-pt-rPT/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-ro-rRO/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-ru-rRU/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-sk-rSK/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-sr-rCS/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-sv-rSE/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-tr-rTR/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-uk-rUA/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-vi-rVN/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-zh-rCN/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values-zh-rTW/strings.xml (100%) rename core/keys/src/{main => androidMain}/res/values/strings.xml (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/BooleanComposedKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/BooleanKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/BooleanNonKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/DoubleKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/DoubleNonKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/IntComposedKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/IntKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/IntNonKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/IntentKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/LongComposedKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/LongNonKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/PreferenceType.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/ProfileComposedBooleanKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/ProfileComposedStringKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/ProfileIntKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/ProtectionType.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/StringKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/StringNonKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/UnitDoubleKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/UnitType.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/BooleanComposedNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/BooleanNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/BooleanPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/ComposedKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/DoubleComposedNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/DoubleNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/DoublePreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/ElementVisibility.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/IntComposedNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/IntNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/LongComposedNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/LongNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/LongPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/NonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/PreferenceEnabledCondition.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/Preferences.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/StringComposedNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/StringNonPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/StringValidator.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/SyncSpec.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/TextRef.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/UnitDoublePreferenceKey.kt (100%) rename core/keys/src/{main => commonMain}/kotlin/app/aaps/core/keys/interfaces/VisibilityContext.kt (100%) diff --git a/core/keys/build.gradle.kts b/core/keys/build.gradle.kts index 44327cb9b65b..670d787fa366 100644 --- a/core/keys/build.gradle.kts +++ b/core/keys/build.gradle.kts @@ -1,50 +1,90 @@ -import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min plugins { - alias(libs.plugins.android.library) - id("android-module-dependencies") - // Test only. composeKey() builds preference key names by hand instead of using String.format, - // so it needs tests that pin the exact text it produces. - id("test-module-dependencies") + kotlin("multiplatform") + // NOT com.android.library. AGP 9 refuses that plugin together with the multiplatform plugin: + // "The 'com.android.library' (or 'com.android.application') plugin is not compatible with the + // 'org.jetbrains.kotlin.multiplatform' plugin since AGP 9.0." + alias(libs.plugins.android.kmp.library) } -android { - namespace = "app.aaps.core.keys" - defaultConfig { +// One task, not one per variant: a multiplatform module has no product flavours, and a Kotlin +// source set takes a task provider directly, so the Android variant API is not needed here. +val generateKeyStrings = tasks.register("generateKeyStrings") { + resDir.set(layout.projectDirectory.dir("src/androidMain/res")) + packageName.set("app.aaps.core.keys") + objectName.set("KeysStrings") + idsObjectName.set("KeysStringIds") + reportFile.set(layout.buildDirectory.file("reports/keyStrings/translations.txt")) + commonOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/android")) +} + +kotlin { + // The Android half. The strings stay in src/androidMain/res and keep being processed by AAPT, + // which is the whole point: Android goes on resolving locales the way it always has, including + // the always English lookup the search index needs. + android { + namespace = "app.aaps.core.keys" + compileSdk = Versions.compileSdk minSdk = min(Versions.minSdk, Versions.wearMinSdk) // Compatible with wear module + // Off by default for a multiplatform library, unlike a plain android library. + androidResources { enable = true } + // Creates the androidHostTest compilation, which also pulls in commonTest. + withHostTest { } + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + + // Restated from android-module-dependencies, which this module can no longer apply. Without + // it MissingTranslation would switch on for the first time here, and the 19 locale files + // that are empty today would fail a release build. The generator's per locale report covers + // the same ground and is on by default, so nothing is actually lost by keeping this off. + lint { + checkReleaseBuilds = false + disable += "MissingTranslation" + disable += "ExtraTranslation" + } } -} -// The key enums name their titles through the generated KeysStrings object rather than R.string, -// so this module stops carrying Android resource ids in its public API. KeysStringIds keeps the -// Android side resolving through AAPT. Both files come from one pass over res/values/strings.xml, -// so they cannot drift apart. See GenerateKeyStringsTask for the reasoning. -extensions.configure("androidComponents") { - onVariants { variant -> - val taskProvider = tasks.register( - "generate${variant.name.replaceFirstChar { it.uppercase() }}KeyStrings", - GenerateKeyStringsTask::class.java - ) { - resDir.set(layout.projectDirectory.dir("src/main/res")) - packageName.set("app.aaps.core.keys") - objectName.set("KeysStrings") - idsObjectName.set("KeysStringIds") - reportFile.set(layout.buildDirectory.file("reports/keyStrings/${variant.name}-translations.txt")) - // Set explicitly. addGeneratedSourceDirectory only applies a convention, and it derives - // that convention from the task name, so both properties would land on the same - // directory and the second file written would delete the first. - commonOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/${variant.name}/common")) - androidOutputDir.set(layout.buildDirectory.dir("generated/keyStrings/${variant.name}/android")) + // Desktop JVM, for a client on Windows. + jvm { + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + } + + // Declared unconditionally. Kotlin/Native cross compiles klibs for Apple targets from any host + // since 2.2.20, so these really do compile on Windows - only linking, cinterop and running + // tests need a Mac, and those tasks report SKIPPED rather than failing. + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + // Platform neutral: 327 string names as TextRef, no Android types. + kotlin.srcDir(generateKeyStrings.flatMap { it.commonOutputDir }) + dependencies { + // project.dependencies.platform, because a Kotlin source set dependency block has + // no platform() of its own. + api(project.dependencies.platform(libs.kotlinx.coroutines.bom)) + api(libs.kotlinx.coroutines.core) + } + } + androidMain { + // Android only: the name to R.string id map. + kotlin.srcDir(generateKeyStrings.flatMap { it.androidOutputDir }) + } + // Hand written rather than taken from test-module-dependencies, because that convention + // plugin applies com.android.library and so cannot be used here. Only what the one test + // in this module actually needs. + getByName("androidHostTest") { + dependencies { + implementation(libs.org.junit.jupiter) + implementation(libs.org.junit.jupiter.api) + implementation(libs.com.google.truth) + runtimeOnly(libs.org.junit.platform.launcher) + } } - // Two directories rather than one: the names are platform neutral and will move to - // commonMain when this module becomes multiplatform, while the id map stays on Android. - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) } } -dependencies { - api(platform(libs.kotlinx.coroutines.bom)) - api(libs.kotlinx.coroutines.core) +tasks.withType { + useJUnitPlatform() } diff --git a/core/keys/src/test/kotlin/app/aaps/core/keys/interfaces/ComposedKeyTest.kt b/core/keys/src/androidHostTest/kotlin/app/aaps/core/keys/interfaces/ComposedKeyTest.kt similarity index 100% rename from core/keys/src/test/kotlin/app/aaps/core/keys/interfaces/ComposedKeyTest.kt rename to core/keys/src/androidHostTest/kotlin/app/aaps/core/keys/interfaces/ComposedKeyTest.kt diff --git a/core/keys/src/main/res/values-ar-rSA/strings.xml b/core/keys/src/androidMain/res/values-ar-rSA/strings.xml similarity index 100% rename from core/keys/src/main/res/values-ar-rSA/strings.xml rename to core/keys/src/androidMain/res/values-ar-rSA/strings.xml diff --git a/core/keys/src/main/res/values-bg-rBG/strings.xml b/core/keys/src/androidMain/res/values-bg-rBG/strings.xml similarity index 100% rename from core/keys/src/main/res/values-bg-rBG/strings.xml rename to core/keys/src/androidMain/res/values-bg-rBG/strings.xml diff --git a/core/keys/src/main/res/values-ca-rES/strings.xml b/core/keys/src/androidMain/res/values-ca-rES/strings.xml similarity index 100% rename from core/keys/src/main/res/values-ca-rES/strings.xml rename to core/keys/src/androidMain/res/values-ca-rES/strings.xml diff --git a/core/keys/src/main/res/values-cs-rCZ/strings.xml b/core/keys/src/androidMain/res/values-cs-rCZ/strings.xml similarity index 100% rename from core/keys/src/main/res/values-cs-rCZ/strings.xml rename to core/keys/src/androidMain/res/values-cs-rCZ/strings.xml diff --git a/core/keys/src/main/res/values-da-rDK/strings.xml b/core/keys/src/androidMain/res/values-da-rDK/strings.xml similarity index 100% rename from core/keys/src/main/res/values-da-rDK/strings.xml rename to core/keys/src/androidMain/res/values-da-rDK/strings.xml diff --git a/core/keys/src/main/res/values-de-rDE/strings.xml b/core/keys/src/androidMain/res/values-de-rDE/strings.xml similarity index 100% rename from core/keys/src/main/res/values-de-rDE/strings.xml rename to core/keys/src/androidMain/res/values-de-rDE/strings.xml diff --git a/core/keys/src/main/res/values-el-rGR/strings.xml b/core/keys/src/androidMain/res/values-el-rGR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-el-rGR/strings.xml rename to core/keys/src/androidMain/res/values-el-rGR/strings.xml diff --git a/core/keys/src/main/res/values-es-rES/strings.xml b/core/keys/src/androidMain/res/values-es-rES/strings.xml similarity index 100% rename from core/keys/src/main/res/values-es-rES/strings.xml rename to core/keys/src/androidMain/res/values-es-rES/strings.xml diff --git a/core/keys/src/main/res/values-fr-rFR/strings.xml b/core/keys/src/androidMain/res/values-fr-rFR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-fr-rFR/strings.xml rename to core/keys/src/androidMain/res/values-fr-rFR/strings.xml diff --git a/core/keys/src/main/res/values-hr-rHR/strings.xml b/core/keys/src/androidMain/res/values-hr-rHR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-hr-rHR/strings.xml rename to core/keys/src/androidMain/res/values-hr-rHR/strings.xml diff --git a/core/keys/src/main/res/values-hu-rHU/strings.xml b/core/keys/src/androidMain/res/values-hu-rHU/strings.xml similarity index 100% rename from core/keys/src/main/res/values-hu-rHU/strings.xml rename to core/keys/src/androidMain/res/values-hu-rHU/strings.xml diff --git a/core/keys/src/main/res/values-it-rIT/strings.xml b/core/keys/src/androidMain/res/values-it-rIT/strings.xml similarity index 100% rename from core/keys/src/main/res/values-it-rIT/strings.xml rename to core/keys/src/androidMain/res/values-it-rIT/strings.xml diff --git a/core/keys/src/main/res/values-iw-rIL/strings.xml b/core/keys/src/androidMain/res/values-iw-rIL/strings.xml similarity index 100% rename from core/keys/src/main/res/values-iw-rIL/strings.xml rename to core/keys/src/androidMain/res/values-iw-rIL/strings.xml diff --git a/core/keys/src/main/res/values-ko-rKR/strings.xml b/core/keys/src/androidMain/res/values-ko-rKR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-ko-rKR/strings.xml rename to core/keys/src/androidMain/res/values-ko-rKR/strings.xml diff --git a/core/keys/src/main/res/values-lt-rLT/strings.xml b/core/keys/src/androidMain/res/values-lt-rLT/strings.xml similarity index 100% rename from core/keys/src/main/res/values-lt-rLT/strings.xml rename to core/keys/src/androidMain/res/values-lt-rLT/strings.xml diff --git a/core/keys/src/main/res/values-nb-rNO/strings.xml b/core/keys/src/androidMain/res/values-nb-rNO/strings.xml similarity index 100% rename from core/keys/src/main/res/values-nb-rNO/strings.xml rename to core/keys/src/androidMain/res/values-nb-rNO/strings.xml diff --git a/core/keys/src/main/res/values-nl-rNL/strings.xml b/core/keys/src/androidMain/res/values-nl-rNL/strings.xml similarity index 100% rename from core/keys/src/main/res/values-nl-rNL/strings.xml rename to core/keys/src/androidMain/res/values-nl-rNL/strings.xml diff --git a/core/keys/src/main/res/values-pl-rPL/strings.xml b/core/keys/src/androidMain/res/values-pl-rPL/strings.xml similarity index 100% rename from core/keys/src/main/res/values-pl-rPL/strings.xml rename to core/keys/src/androidMain/res/values-pl-rPL/strings.xml diff --git a/core/keys/src/main/res/values-pt-rBR/strings.xml b/core/keys/src/androidMain/res/values-pt-rBR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-pt-rBR/strings.xml rename to core/keys/src/androidMain/res/values-pt-rBR/strings.xml diff --git a/core/keys/src/main/res/values-pt-rPT/strings.xml b/core/keys/src/androidMain/res/values-pt-rPT/strings.xml similarity index 100% rename from core/keys/src/main/res/values-pt-rPT/strings.xml rename to core/keys/src/androidMain/res/values-pt-rPT/strings.xml diff --git a/core/keys/src/main/res/values-ro-rRO/strings.xml b/core/keys/src/androidMain/res/values-ro-rRO/strings.xml similarity index 100% rename from core/keys/src/main/res/values-ro-rRO/strings.xml rename to core/keys/src/androidMain/res/values-ro-rRO/strings.xml diff --git a/core/keys/src/main/res/values-ru-rRU/strings.xml b/core/keys/src/androidMain/res/values-ru-rRU/strings.xml similarity index 100% rename from core/keys/src/main/res/values-ru-rRU/strings.xml rename to core/keys/src/androidMain/res/values-ru-rRU/strings.xml diff --git a/core/keys/src/main/res/values-sk-rSK/strings.xml b/core/keys/src/androidMain/res/values-sk-rSK/strings.xml similarity index 100% rename from core/keys/src/main/res/values-sk-rSK/strings.xml rename to core/keys/src/androidMain/res/values-sk-rSK/strings.xml diff --git a/core/keys/src/main/res/values-sr-rCS/strings.xml b/core/keys/src/androidMain/res/values-sr-rCS/strings.xml similarity index 100% rename from core/keys/src/main/res/values-sr-rCS/strings.xml rename to core/keys/src/androidMain/res/values-sr-rCS/strings.xml diff --git a/core/keys/src/main/res/values-sv-rSE/strings.xml b/core/keys/src/androidMain/res/values-sv-rSE/strings.xml similarity index 100% rename from core/keys/src/main/res/values-sv-rSE/strings.xml rename to core/keys/src/androidMain/res/values-sv-rSE/strings.xml diff --git a/core/keys/src/main/res/values-tr-rTR/strings.xml b/core/keys/src/androidMain/res/values-tr-rTR/strings.xml similarity index 100% rename from core/keys/src/main/res/values-tr-rTR/strings.xml rename to core/keys/src/androidMain/res/values-tr-rTR/strings.xml diff --git a/core/keys/src/main/res/values-uk-rUA/strings.xml b/core/keys/src/androidMain/res/values-uk-rUA/strings.xml similarity index 100% rename from core/keys/src/main/res/values-uk-rUA/strings.xml rename to core/keys/src/androidMain/res/values-uk-rUA/strings.xml diff --git a/core/keys/src/main/res/values-vi-rVN/strings.xml b/core/keys/src/androidMain/res/values-vi-rVN/strings.xml similarity index 100% rename from core/keys/src/main/res/values-vi-rVN/strings.xml rename to core/keys/src/androidMain/res/values-vi-rVN/strings.xml diff --git a/core/keys/src/main/res/values-zh-rCN/strings.xml b/core/keys/src/androidMain/res/values-zh-rCN/strings.xml similarity index 100% rename from core/keys/src/main/res/values-zh-rCN/strings.xml rename to core/keys/src/androidMain/res/values-zh-rCN/strings.xml diff --git a/core/keys/src/main/res/values-zh-rTW/strings.xml b/core/keys/src/androidMain/res/values-zh-rTW/strings.xml similarity index 100% rename from core/keys/src/main/res/values-zh-rTW/strings.xml rename to core/keys/src/androidMain/res/values-zh-rTW/strings.xml diff --git a/core/keys/src/main/res/values/strings.xml b/core/keys/src/androidMain/res/values/strings.xml similarity index 100% rename from core/keys/src/main/res/values/strings.xml rename to core/keys/src/androidMain/res/values/strings.xml diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanComposedKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanComposedKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/BooleanComposedKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanComposedKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/BooleanNonKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanNonKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/BooleanNonKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/BooleanNonKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/DoubleKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/DoubleKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/DoubleKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/DoubleNonKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/DoubleNonKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/DoubleNonKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/DoubleNonKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntComposedKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntComposedKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/IntComposedKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntComposedKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/IntKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntNonKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntNonKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/IntNonKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntNonKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntentKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/IntentKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/IntentKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/LongComposedKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/LongComposedKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/LongComposedKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/LongComposedKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/LongNonKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/LongNonKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/LongNonKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/LongNonKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/PreferenceType.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/PreferenceType.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/PreferenceType.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/PreferenceType.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/ProfileComposedBooleanKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileComposedBooleanKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/ProfileComposedBooleanKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileComposedBooleanKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/ProfileComposedStringKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileComposedStringKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/ProfileComposedStringKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileComposedStringKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/ProfileIntKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileIntKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/ProfileIntKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProfileIntKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/ProtectionType.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProtectionType.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/ProtectionType.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/ProtectionType.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/StringKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/StringKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/StringKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/StringNonKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/StringNonKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/StringNonKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/StringNonKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/UnitDoubleKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/UnitDoubleKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/UnitDoubleKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/UnitType.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/UnitType.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/UnitType.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanComposedNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanComposedNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanComposedNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanComposedNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/BooleanPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/BooleanPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/ComposedKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/ComposedKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/ComposedKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/ComposedKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoubleComposedNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoubleComposedNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoubleComposedNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoubleComposedNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoubleNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoubleNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoubleNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoubleNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoublePreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoublePreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/DoublePreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/DoublePreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/ElementVisibility.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/ElementVisibility.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/ElementVisibility.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/ElementVisibility.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntComposedNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntComposedNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntComposedNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntComposedNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongComposedNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongComposedNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongComposedNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongComposedNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/LongPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/LongPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/NonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/NonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/NonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/NonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceEnabledCondition.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/PreferenceEnabledCondition.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceEnabledCondition.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/PreferenceEnabledCondition.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/PreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/Preferences.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/Preferences.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/Preferences.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringComposedNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringComposedNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringComposedNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringComposedNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringNonPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringNonPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringNonPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringNonPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringPreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringValidator.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringValidator.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/StringValidator.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/StringValidator.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/SyncSpec.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/SyncSpec.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/SyncSpec.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/SyncSpec.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/TextRef.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/UnitDoublePreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/UnitDoublePreferenceKey.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/UnitDoublePreferenceKey.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/UnitDoublePreferenceKey.kt diff --git a/core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/VisibilityContext.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/VisibilityContext.kt similarity index 100% rename from core/keys/src/main/kotlin/app/aaps/core/keys/interfaces/VisibilityContext.kt rename to core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/VisibilityContext.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3bc6ce836cb..fc9d88342401 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,10 @@ glance = "1.1.1" [plugins] android-library = { id = "com.android.library" } +# No version, exactly like android-library above: both plugin ids ship in com.android.tools.build:gradle, +# which is already on the root buildscript classpath. AGP 9 refuses com.android.library together with +# the Kotlin multiplatform plugin, so a multiplatform module with an Android target needs this one. +android-kmp-library = { id = "com.android.kotlin.multiplatform.library" } klint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.2.0" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" } ksp = { id = "com.google.devtools.ksp", version = "2.3.10" } diff --git a/runtests.bat b/runtests.bat index 2f0146454568..eb4cc1d5018e 100644 --- a/runtests.bat +++ b/runtests.bat @@ -1 +1,3 @@ -gradlew -Pcoverage -PfirebaseDisable testFullDebugUnitTest \ No newline at end of file +@rem allTests is needed as well as testFullDebugUnitTest: multiplatform modules have no build +@rem variants, so they have no testFullDebugUnitTest task and would silently run no tests at all. +gradlew -Pcoverage -PfirebaseDisable testFullDebugUnitTest allTests diff --git a/runtests.sh b/runtests.sh index 288ac90dda8a..b9e48f386bcb 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,2 +1,4 @@ #!/bin/zsh -./gradlew -Pcoverage -PfirebaseDisable testFullDebugUnitTest +# allTests is needed as well as testFullDebugUnitTest: multiplatform modules have no build +# variants, so they have no testFullDebugUnitTest task and would silently run no tests at all. +./gradlew -Pcoverage -PfirebaseDisable testFullDebugUnitTest allTests From 9667f80d4f2ee092df788d40fc46983ae263b5d3 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 17:50:03 +0200 Subject: [PATCH 020/146] Update progress --- _docs/KMP_IOS_FEASIBILITY.md | 330 +++++++++++++++++++++++++---------- 1 file changed, 238 insertions(+), 92 deletions(-) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index d43522f9c0e6..0f4ad91cec9e 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -12,9 +12,13 @@ build folders excluded. ## 1. Short answer -There is **no KMP setup in the project today**. No module uses the multiplatform plugin. -(`:pump:combov2:comboctl` has `commonMain` / `androidMain` folder names left over from the upstream -project, but it builds as a normal Android library.) +*(Written when there was no KMP setup at all. Three modules are multiplatform now - `:core:data`, +`:core:nssdk` and `:core:keys` - and the last of them compiles for real iOS targets. See section 7 +for where things actually stand; the rest of this section is kept because the reasoning still holds.)* + +There was **no KMP setup in the project** when this note was written. No module used the +multiplatform plugin. (`:pump:combov2:comboctl` has `commonMain` / `androidMain` folder names left +over from the upstream project, but it builds as a normal Android library.) The shape of the code is much better than in a typical Android app, mostly because of the Compose migration and the RxJava removal. The realistic path is **Kotlin Multiplatform with Compose @@ -37,13 +41,21 @@ Multiplatform for the UI**, done step by step, starting with a small working sli | `androidx.lifecycle` ViewModel | Multiplatform since 2.8 | Most of the 129 uses are fine | | Vico charts | Ships a `multiplatform` artifact | Only the artifact name changes | -The biggest surprise was `:core:ui`. Of its 432 files: +The biggest surprise was `:core:ui`. Of its 434 files: - 424 are under `compose/` - **0** import Dagger or `javax.inject` - 27 import `app.aaps.core.interfaces` - 34 import `app.aaps.core.keys` -- **361 (84%) import none of Android, Dagger or the AAPS interfaces** +- **only 16 import `android.*`** + +Wave 14 re-measured that last line, because it is the one that decides whether a shared Compose UI is +realistic, and an earlier count of "427 of 434" was wrong - it grepped `^import android` without the +dot, which also matches `androidx`. The real figure is 16. The other 424 import `androidx.*`, and +almost all of those are `androidx.compose.*`, which Compose Multiplatform publishes under the *same +package names*. What is left is a tail of about 26 non-Compose androidx imports - +`core.graphics` (6), `lifecycle.compose` (5), `annotation` (3), `activity.compose` (3), and single +uses of `fragment.app`, `appcompat.app`, `core.view`, `core.content` and `activity.result`. So most of the Compose work of the last year can be reused. @@ -317,6 +329,14 @@ This is real work, not an afterthought. Plan for it. ## 6. Resources and translations +> **Superseded by wave 14 - compose-resources is not being used.** This section works through what +> adopting it would mean, and the conclusion turned out to be wrong for a reason it never considered: +> the same `values-XX` folder names *parse* under compose-resources but do not *match* a request that +> carries no region, which is what this app sends for 22 of its 25 languages. The current answer is +> simpler than anything below - **Android keeps AAPT and nothing moves**, and only a platform neutral +> name crosses the boundary. The three problems listed further down are still real, and are still the +> reasons compose-resources was rejected. Keep reading for those; ignore the migration plan. + **Translations survive almost unchanged.** Compose Multiplatform Resources uses the same `strings.xml` format and the same `values-XX` folder names as Android. @@ -396,28 +416,29 @@ Step 0 proves the toolchain works, gives an honest answer about how Compose Mult iOS, and makes every later step demand driven: a blocker is removed because it stands between you and the next screen, not because it is on a list. -**Where this stands.** Steps 1 and 5 are **done**, and step 0 is half done - out of order, because -`:core:nssdk` turned out to be sliceable after all. Two modules now build for Kotlin/Native: +**Where this stands.** Steps 1, 2 and 5 are **done**, and step 0 is half done - out of order, because +`:core:nssdk` turned out to be sliceable after all. Three modules now build for Kotlin/Native: -| Module | State | -|---------------|-----------------------------------------------------| -| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | -| `:core:nssdk` | multiplatform, 72 files in `commonMain` (waves 6-9) | +| Module | State | +|---------------|----------------------------------------------------------------------------| +| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | +| `:core:nssdk` | multiplatform, 72 files in `commonMain` (waves 6-9) | +| `:core:keys` | multiplatform, 47 files in `commonMain`, **real iOS targets** (wave 14) | Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Gson, joda, Retrofit, OkHttp, `android.*` and `java.io` are all gone from it. -**Step 2 is prepared but not done.** `:core:keys` now hands out `TextRef` rather than bare resource -ids (wave 10), owns only strings it actually uses (wave 11), and nothing outside it reads its `R` -class (wave 12). The spike proved the toolchain works (wave 13). What has **not** happened is the -conversion itself - the module is still an Android library with `res/values*` and a generated `R`. -Resume instructions are in section 9b. +**Step 2 is done (wave 14).** `:core:keys` hands out `TextRef` rather than bare resource ids +(wave 10), owns only strings it actually uses (wave 11), nothing outside it reads its `R` class +(wave 12), and it is now a `com.android.kotlin.multiplatform.library` targeting android + jvm + +iosArm64 + iosSimulatorArm64. Its strings never moved: they are still AAPT resources in +`src/androidMain/res`, and a generated name/id pair is what crosses the platform boundary. -That leaves the rest of steps 2, 3, 4 and 6. Step 0's remaining half **needs a Mac** - or a -`macos-latest` CI runner, which is how the projects with public precedent do it and which is free for -a public repo. No amount of further blocker removal answers how Compose Multiplatform actually feels -on iOS. +That leaves steps 3, 4 and 6. **Step 0's remaining half is smaller than this note assumed**: Apple +klibs cross compile on Windows, so "does it compile for iOS" is answerable locally and already +answered for three modules. What still needs a Mac is running iOS tests, linking a framework, and the +only question that really matters - how Compose Multiplatform *feels* on a real iPhone. --- @@ -446,6 +467,13 @@ worth keeping (see waves 5 and 6): | `e92ec082d3` | `:core:nssdk` tests - the contract suite, written against Retrofit before the port | | `ed9c87599f` | `:core:nssdk` Ktor migration | | `37e146861f` | version `4.0.0-dev-b-kmp` | +| `307b1ed615` | `TextRef` (wave 10) | +| `defd131dec` | `:core:keys` eliminate dependencies (wave 11) | +| `95da0fa7ca` | more `TextRef` migration (wave 11) | +| `6fdb924e6b` | `:core:keys` String migration (wave 12) | +| `c8c1069817` | Fix Danish language selection - `dk` matched no folder, see section 9a | +| `29dd6ff33e` | `:core:keys` `TextRef.Named` - generated names + id map, 342 call sites (wave 14) | +| `eeddd8182c` | `:core:keys` kmp - multiplatform module, real iOS targets (wave 14) | ### Wave 1 - `DecimalFormat` removed @@ -1324,14 +1352,23 @@ can then only be compiled on macOS. Two corrections to earlier reasoning in this note: -- `ResourceEnvironment`'s constructor is **public** in 1.11.1 (it was `internal` when wave 10 was - written). So an always-English `ResourceEnvironment(LanguageQualifier("en"), ...)` is possible, and - the search index's English half is **not** a blocker any more. That was the original reason for - rejecting `StringResource` on the key classes. +- ~~`ResourceEnvironment`'s constructor is **public** in 1.11.1~~ - **this was wrong, and it was the + load bearing claim.** Wave 14 downloaded the published `components-resources:1.11.1` sources jar + and read it: the declaration is `class ResourceEnvironment internal constructor(...)`. The only + public producer is `getSystemResourceEnvironment()`, which reads `Locale.getDefault()`. So there is + still **no way to ask for a specific locale**, and the always English search index still cannot be + built. Wave 10's original objection stood the whole time. 1.12.0-beta03 does not fix it either - + the constructor is still internal and has gained a fifth parameter. - Holding a `StringResource` costs **kotlin-stdlib only** - `components-resources` declares nothing - else in `apiElements`; Compose appears only in `runtimeElements`. + else in `apiElements`; Compose appears only in `runtimeElements`. (Still true.) -Still true: **every `getString` overload is `suspend`.** There is no synchronous variant. +Still true: **every `getString` overload is `suspend`.** There is no synchronous variant. Wave 14 +adds one more: `stringResource()` is a **blocking** read on every platform except JS, including +inside composition. + +**The spike also asked the wrong question about locale folders.** It checked that `values-de-rDE` +*parses* into a `LanguageQualifier` plus a `RegionQualifier`. It never checked that such a folder +*matches* a request. It does not, when the request carries no region - see wave 14. There is real precedent on this exact toolchain: **Meshtastic-Android** runs a dedicated `:core:resources` module (1747 strings, ~40 locales, `publicResClass = true`) on AGP 9.3.1 / @@ -1339,6 +1376,112 @@ Kotlin 2.4.10 / CMP 1.11.1, and **Todometer-KMP** targets android + jvm + iosArm iosSimulatorArm64. Both build iOS only on `macos-latest` runners - which is the answer for a maintainer with no Mac, and free for a public repo. +### Wave 14 - `:core:keys` is multiplatform, and the string plan changed (done) + +Committed as `c8c1069817`, `29dd6ff33e` and `eeddd8182c`. This wave overturned the destination that +waves 10 to 13 had been walking towards, so the reasoning matters more than the diff. + +#### compose-resources cannot serve this app, and no version of it can + +Two facts, both read out of the **published** `components-resources:1.11.1` sources jar rather than a +docs page or a GitHub branch: + +- `class ResourceEnvironment internal constructor(...)`. The only public producer is + `getSystemResourceEnvironment()`, which reads `Locale.getDefault()`. There is no way to ask for + English while the UI is in another language. Wave 13 claimed this had been fixed; it had not, and + 1.12.0-beta03 still has it internal, now with a fifth parameter. +- `filterByLocale` has exactly three steps, and the source comment says so: exact language+region, + then language **with no region qualifier**, then no locale qualifiers at all. There is no + sibling-region fallback. + +That second one is the fatal one, and nobody had looked at it. `LocaleHelper.currentLocale()` builds +a **region-less** `Locale` for 22 of the 25 in-app languages, and every `:core:keys` folder is +region-pinned (`values-cs-rCZ`; no bare `values-cs` exists anywhere). Walk the algorithm: step 1 +needs a region and there is none, step 2 needs a folder without a region and there is none, so it +falls through to the unqualified default - **English**. Concretely, 8 of the 11 translated locales +would have shown English preference screens: bg, cs, es, fr, it, nb, ro, sk. It also breaks device +locales whose region differs from the folder - de_AT, fr_CA, es_MX, nl_BE, zh_HK - which AAPT +resolves correctly today. + +Build green, no crash, no test catches it, and CI runs no tests. This is exactly the failure shape +section 9a describes, and it would have been introduced deliberately. + +#### moko-resources was the better library and still lost + +It does no locale matching of its own: it generates real `values-XX` files and emits +`StringResource(R.string.key)`, so Android keeps AAPT semantics, and `StringDesc.LocaleType.Custom` +is a real locale override. Both defects gone. But `0.26.4` shipped 2026-05-06, one month **before** +Kotlin 2.4.0, with no commits since, 175 open issues and a catalog pinned to Kotlin 2.1.0/2.3.20. A +medical app's string layer is the wrong place for an unattended dependency. + +#### What was built instead + +The insight is that the module shape was never the blocker. `:core:data` has been multiplatform since +wave 5 and 39 modules consume it with a plain `implementation(project(...))`. What blocked +`:core:keys` was the `R.string` **`Int`** inside 6 enum files - a *content* problem. + +`buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt` makes one pass over `res/values/strings.xml` and +emits two files: + +| Generated | Where | What | +|-----------|-------|------| +| `KeysStrings` | `commonMain` | 327 `val x: TextRef = TextRef.Named("x")` - no Android types | +| `KeysStringIds` | `androidMain` | 327 `"x" to R.string.x`, plus `idOf(name)` | + +Same pass, so they cannot drift: a string deleted from the XML disappears from both and any call site +naming it stops compiling. `TextRef` gained `Named(name, args)`; the 6 enums now take +`override val title: TextRef` directly and the computed `AndroidRes` properties are gone; 342 +references were rewritten; the three resolvers gained a `Named` branch. + +The KDoc in `TextRef.kt` that argued against names was **conditionally right and absolutely wrong**. +It said a name needs `Resources.getIdentifier()` - reflective, invisible to R8, silently 0 for a +typo. All three objections die against a *generated* map: no reflection, R8 sees 327 literal +`R.string.x` references, and a typo does not compile. + +Net effect: **zero resource files moved, `crowdin.yml` untouched, AAPT still resolves every locale, +and `gsNotLocalised` still works.** Only one of the 19 production `gsNotLocalised` call sites touches +`:core:keys` at all - `SearchIndexBuilder.kt:340`. The other 18 resolve `:plugins:sync` and +plugin-name strings and were never at risk. + +#### The module flip, and three things it taught + +`com.android.kotlin.multiplatform.library`, `android { }` **inside** `kotlin { }`, +`androidResources { enable = true }`, targets android + jvm + iosArm64 + iosSimulatorArm64. Sources +went to `commonMain` (47), `androidMain` (res), `androidHostTest` (1 test); git recorded every one as +a pure rename. + +- **The flavour fear was unfounded.** That AGP extension really does expose no `productFlavors`, but + AGP matches a flavourless library to any app variant. All 5 flavours of `:app` and `:wear` build. +- **`platform()` does not exist** in a Kotlin source set dependency block. Use + `project.dependencies.platform(...)`. +- **Dropping `android-module-dependencies` silently switches `MissingTranslation` on** and restores + `checkReleaseBuilds` to its default `true`. With 19 empty locales that could fail a release build, + so the `lint { }` block has to be restated in the module. + +#### iOS compiles here + +Kotlin/Native has cross compiled klibs for Apple targets from any host since 2.2.20, on by default - +`PropertiesProvider.kt:595` reads `... ?: true`. `compileKotlinIosArm64` and +`compileKotlinIosSimulatorArm64` genuinely **execute** on the Windows machine this project is +developed on and produce real klibs, against the 177 prebuilt `ios_arm64` klibs already sitting in +`~/.konan`. Nothing is fetched from Apple and Xcode is not involved. + +A Mac is still needed to **run** iOS tests, to link frameworks and XCFrameworks, and for cinterop. +Those tasks are `enabled = false` and report **SKIPPED**, not failed - so a build stays green even if +the entire Apple side silently stops compiling. Any CI job must assert the task *executed*. + +#### Verified + +`:app` + `:wear` green across all 5 flavours; `testFullDebugUnitTest allTests` green; the resource +baseline is byte-identical before and after the move (3924 elements, 31 dirs, 12/12 name lists); and +on an emulator the English screens render titles and summaries, Czech renders `:core:keys` strings in +Czech, and Danish renders Danish. + +`runtests.bat` / `.sh` now run `testFullDebugUnitTest allTests`. Without `allTests` a multiplatform +module has no `testFullDebugUnitTest` task and silently runs **no tests at all** - which is what had +been happening to `:core:data` and `:core:nssdk` since waves 5 to 9, unnoticed because +`failOnNoDiscoveredTests = false`. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old @@ -1448,12 +1591,13 @@ keeps the old contract on a public interface method. 13. **Still open: does web ever matter?** It is the one target that changes the **API shape** rather than just the implementation - WebCrypto is async-only, so `sign()` and `wrap()` would have to become `suspend`. Cheap to decide now, expensive to retrofit. -14. ~~compose-resources, moko-resources, or something else for strings?~~ **Decided: `TextRef` now, - compose-resources later, per module.** The two are not alternatives. compose-resources is still - the destination - same `strings.xml`, same Crowdin - but it cannot go on the key classes today, - because `getString()` is `suspend` and there is no public locale override, which would cost the - English search index. `TextRef` is the seam that lets each module move on its own schedule - without touching call sites twice. See wave 10. +14. ~~compose-resources, moko-resources, or something else for strings?~~ **Reopened and decided + again in wave 14: neither. Android keeps AAPT, and only a platform neutral *handle* is shared.** + compose-resources is not the destination after all - it cannot match a region-less locale against + a region-pinned folder, and it has no locale override, so it would have broken 8 of the 11 + translated locales and the always English search index at the same time. moko-resources solves + both, but its latest release predates Kotlin 2.4 with no commits since. `TextRef` is still the + seam, and it is now what makes the answer cheap: the backend is hidden from 342 call sites. 15. ~~Collapse `IntPreferenceKey.entries` / `resolvedEntries`.~~ **Done in wave 11.** `entries` is `Map`, `resolvedEntries` is deleted, `withEntries` takes `TextRef` - which is what let the Eopatch `"$it U"` concatenation become a translated template. @@ -1464,21 +1608,30 @@ keeps the old contract on a public interface method. 17. ~~Does `:core:keys` need its own strings?~~ **Yes, structurally.** It is a leaf module - zero project dependencies - and `:core:ui` depends on **it**. So the strings cannot move to `:core:ui` (cycle), and a key naming its own title at compile time means they must live in the same module. - That is what makes compose-resources the answer rather than a relocation. + What changed in wave 14 is only *how* they are named: the strings stayed exactly where they were, + in `res/values*`, and the module became multiplatform around them. 18. ~~compose-resources on `:core:keys`, or negative-integer tokens in `TextRef`?~~ - **Decided: compose-resources.** The token scheme's whole advantage was keeping `mingwX64`, and - `mingwX64` is not in the real target set (Android + iOS + JVM-on-Windows). Tokens would also be a - bespoke design with no public precedent, which is the wrong trade when iOS cannot be compiled - locally. See wave 13. + **Overturned in wave 14. Neither: `TextRef.Named` plus a generated id map.** The conclusion that + tokens were wrong survives - they encode a handle without providing a table behind it - but + compose-resources turned out to be worse, not better. A *generated* name is what the reasoning + missed: it is not the reflective `getIdentifier()` lookup that the token scheme was invented to + avoid, and it needs no resource framework at all on the platform that already has one. 19. **Still open: when to enable `MissingTranslation` lint.** It is disabled repo-wide and is the - reason 19 languages silently lost `:core:keys`. Needs to be on - at warning level with a CI - report - before any further string relocation. See section 9a. -20. **Still open: `:core:ui` is next after `:core:keys`.** It holds 1063 strings and every module - depends on it, so the same questions return at a larger scale. Worth deciding whether it converts - module-by-module or whether a dedicated shared-strings leaf is better at that point. - -Waves 1 to 4 are committed on `dev`. Waves 5 to 13 are committed on `kmp/core-data-experiment` -(HEAD `6fdb924e6b`), which is still **13+ commits ahead and 0 behind `dev` - a fast-forward**. + reason 19 languages silently lost `:core:keys`. Wave 14 makes this less urgent but not moot: the + generator now prints per locale completeness on every build of `:core:keys`, so that one module + is covered. The other 32 string owning modules still have no detector. +20. **Still open: what comes after `:core:keys`.** It holds 1063 strings and every module depends on + it, so the string question returns at a larger scale - though wave 14's answer scales with it, + because nothing has to move. The bigger question for `:core:ui` is not strings at all: it is 16 + files importing `android.*` and a handful of non-Compose `androidx` artifacts. +21. ~~Do Apple targets need a Mac to compile?~~ **No - decided in wave 14.** Kotlin/Native has cross + compiled klibs for Apple targets from any host since 2.2.20, on by default. `:core:keys` compiles + for `iosArm64` and `iosSimulatorArm64` on the Windows machine this project is developed on. A Mac + is still needed to *run* iOS tests, to link frameworks, and for cinterop. + +Waves 1 to 4 are committed on `dev`. Waves 5 to 14 are committed on `kmp` (HEAD `eeddd8182c`), which +is now **20 ahead and 1 behind `dev`**. It stopped being a fast-forward when `dev` moved, so the +merge in open decision 11 is a real merge and gets no cheaper by waiting. Waves 5 to 9 were each verified against a live Nightscout before the next started - waves 5 and 6 on WSA, waves 7 to 9 on an emulator running a new master against a **pre-KMP client**, so every step was @@ -1498,6 +1651,19 @@ The `FoodManagement` comma defect in section 10 is found but not fixed. **This is on `dev` today, it predates all KMP work, and nothing in the build can see it.** +> **A second, separate translation bug was found while checking this one, and is now fixed** +> (`c8c1069817`). The in-app language list offered `"dk"` for Danish, but `dk` is a country code - +> the ISO 639 language code is `da`, and all 32 modules keep their Danish text in `values-da-rDK`. +> No `values-dk` folder exists anywhere in the repository, so `Locale.Builder().setLanguage("dk")` +> matched nothing and **picking Danish rendered the entire app in English**. The translations were +> there the whole time and simply unreachable. Fixed at the single choke point, +> `LocaleHelper.currentLocale()`, which now maps `dk` to `da`; the stored preference value is left +> alone, because `GeneralLanguage` syncs between master and client. Device-verified. +> +> Two more offered languages have no folders anywhere - `af` (Afrikaans) and `ga` (Irish) - but +> nothing is lost there, because they were never translated. The picker simply promises more than it +> can deliver. + `core/keys/src/main/res/values/strings.xml` has 327 strings. Only **11 locales** have translations: bg, cs, es, fr, it, nb, ro, sk, vi, zh-rCN, zh-rTW. The other **19 locale files exist but contain zero `` elements**: de, ru, pl, nl, pt-rBR, pt-rPT, tr, sv, uk, ar, ca, da, el, hr, hu, iw, @@ -1552,59 +1718,39 @@ Useful commands are in the session notes: the CLI needs `-b dev`, a minimal temp --- -## 9b. Where to resume - `:core:keys` to compose-resources - -Everything up to here is committed. The conversion itself has **not** started. - -### Stage 2b - convert the module - -1. **`core/keys/build.gradle.kts`** - replace the Android-library setup with: - `kotlin("multiplatform")` + `org.jetbrains.compose` + `org.jetbrains.kotlin.plugin.compose` - (the Compose plugin **hard-fails** without the compiler plugin) + `com.android.library`. - Targets: `androidTarget()`, `jvm()`, `iosArm64()`, `iosSimulatorArm64()`. **No `mingwX64`** - - `components-resources` does not publish it. - `compose.resources { publicResClass = true; packageOfResClass = "app.aaps.core.keys.resources"; - generateResClass = always }`, and `android { androidResources.enable = true }`. - Add `api(compose.components.resources)` - `api`, because the type is in the public API. -2. **Move sources** `src/main/kotlin` -> `src/commonMain/kotlin` (47 files). -3. **Move resources** `src/main/res/values*/strings.xml` -> - `src/commonMain/composeResources/values*/strings.xml`. **Folder names stay exactly as they are** - - the spike proved `values-de-rDE` etc. parse correctly. -4. **Add `TextRef.Res(resource: StringResource, args: List)`** next to `AndroidRes` and - `Literal`. -5. **~340 `R.string.x` -> `Res.string.x`** in the 6 enum files. The generated class cannot be named - `R`, so this is a real edit, not a package alias. -6. Watch `minSdk = min(Versions.minSdk, Versions.wearMinSdk)` - `:wear` consumes this module, and a - KMP module with `androidTarget()` consumed by `:wear` is the part with least public precedent. - -### Stage 3 - resolvers - -- Compose: `stringResource(TextRef)` in `:core:ui` gains a `Res` branch. -- Non-Compose: `ResourceHelper.gs(TextRef)` must stay **synchronous** because ~15 call sites and the - search index are not suspend. `getString` is suspend, so use a **lazy cache**: - `ConcurrentHashMap`, filled with `getOrPut { runBlocking { getString(...) } }`, - so the blocking read happens at most once per string. Pair it with a **background warm** at startup - so the main thread rarely pays it. A second cache keyed on the English `ResourceEnvironment` serves - `gsNotLocalised` for the search index. -- Do **not** make `SearchIndexBuilder.getIndex()` suspend - the cache removes the need, and the - suspend route would spread to ~15 other synchronous callers. +## 9b. ~~Where to resume - `:core:keys` to compose-resources~~ Superseded by wave 14 + +**The recipe that used to be here would not have built.** It applied `com.android.library` next to +`kotlin("multiplatform")`, which AGP 9 refuses outright, and it moved 30 locale folders into +`composeResources`, where they would have stopped matching the app's own language setting. It is kept +only as a record of what was planned; what actually happened is wave 14, and the current state is: + +- `:core:keys` **is** a multiplatform module - `com.android.kotlin.multiplatform.library`, targets + android + jvm + iosArm64 + iosSimulatorArm64, all 47 sources in `commonMain`. +- Its strings **did not move**. They are still `src/androidMain/res/values*/strings.xml`, still + processed by AAPT, still the same paths in `crowdin.yml`. +- No `R.string` id appears in its public API any more. ### Follow-ups, in rough priority order -1. **Merge `kmp/core-data-experiment` into `dev`** - still a fast-forward, and it gets less free - every day. (Open decision 11.) -2. **The translation work in section 9a** - before any further string move. -3. **A `macos-latest` CI job** building `iosSimulatorArm64`. Nothing verifies the Native side today; - CI is all `ubuntu-latest` running only `:app:assemble`. -4. **`PluginDescription.description: Int`** - the `-1` sentinel, set by 57 files. Must become - `TextRef` before plugins convert. -5. **Remaining `!= 0` / `!= -1` resource sentinels** - `SearchableItem` (2), `MainDrawer`, +1. **Merge `kmp` into `dev`** - no longer a fast-forward, and it gets less free every day. + (Open decision 11.) +2. **The translation work in section 9a** - 19 of 30 locales are still empty shells. +3. **`PluginDescription.description: Int`** - the `-1` sentinel, set by 57 files. It is now a smaller + job than it was: `TextRef.Named` exists, and the pattern for generating names is in `buildSrc`. +4. **Remaining `!= 0` / `!= -1` resource sentinels** - `SearchableItem` (2), `MainDrawer`, `ManageBottomSheet`, `TreatmentBottomSheet`, `PreferenceScreenView`, `QuickLaunchResolver`, `InfoStep`, `SWEventListener`. Harmless today. -6. **`IntPreferenceKey.entries` in the pump modules** still use `entriesResIds`; fine while pumps stay +5. **`IntPreferenceKey.entries` in the pump modules** still use `entriesResIds`; fine while pumps stay Android. -7. **Swap `mingwX64` for Apple targets in `:core:data` / `:core:nssdk`** once macOS CI exists - they - currently prove themselves against a platform we do not ship. +6. **Swap `mingwX64` for Apple targets in `:core:data` / `:core:nssdk`.** No longer gated on macOS CI + - Apple klibs cross compile on Windows. For `:core:nssdk` the engine moves from `ktor-client-cio` + in `mingwX64Main` to `ktor-client-darwin` in `appleMain`. +7. **A `macos-latest` CI job.** Now buys less than it used to, because compiling for Apple already + happens locally - but it is still the only place iOS tests can *run* and frameworks can link. +8. **A resolver for `TextRef.Named` off Android.** `KeysStrings` is in `commonMain`, but the id map + that turns a name into text is `androidMain`. Nothing needs this until there is a non-Android UI, + and the client only needs the system language, so it is a small generated table when it comes. ### Environment notes for another machine From a4291a17ccefb83de2038385af4d0c89f64fc084 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 19:13:49 +0200 Subject: [PATCH 021/146] :core:data ios target --- core/data/build.gradle.kts | 33 ++++++++----- .../data/format/NumberFormatPlatform.ios.kt | 48 +++++++++++++++++++ .../aaps/core/data/time/SystemTimeZone.ios.kt | 18 +++++++ gradle/libs.versions.toml | 1 + 4 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt create mode 100644 core/data/src/iosMain/kotlin/app/aaps/core/data/time/SystemTimeZone.ios.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index c458a095d4c9..e1161b68a3b7 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { kotlin("multiplatform") } @@ -11,17 +13,26 @@ kotlin { } } - // Stand-in for a real Kotlin/Native target. iOS targets need macOS and Xcode, which cannot run - // on Windows, but mingwX64 compiles the same common code through Kotlin/Native, so it proves - // there is no JVM API left in commonMain. Replace or extend with - // iosArm64() / iosSimulatorArm64() on a Mac. - mingwX64 { - compilerOptions { - // kotlin.assert has an experimental implementation on Native. ICfg.iobCalcForTreatment - // uses it. Opting in here keeps that code exactly as it is - turning the asserts into - // require() would change behaviour, because JVM assertions are off in production while - // require() always throws. - optIn.add("kotlin.experimental.ExperimentalNativeApi") + // Real Apple targets. Kotlin/Native cross compiles klibs for them from any host, so these do + // build on Windows - only linking, cinterop and running their tests need a Mac. + iosArm64() + iosSimulatorArm64() + + // mingwX64 stays, and not out of habit: it is the only Kotlin/Native target whose tests can + // actually RUN on this machine. iosSimulatorArm64Test is disabled off macOS ("simulator tests + // require macOS"), so without mingw there would be no way to execute common code through + // Kotlin/Native at all before a Mac appears. + mingwX64() + + targets.withType().configureEach { + compilations.configureEach { + compileTaskProvider.configure { + // kotlin.assert has an experimental implementation on Native. ICfg.iobCalcForTreatment + // uses it. Opting in here keeps that code exactly as it is - turning the asserts into + // require() would change behaviour, because JVM assertions are off in production while + // require() always throws. + compilerOptions.optIn.add("kotlin.experimental.ExperimentalNativeApi") + } } } diff --git a/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt b/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt new file mode 100644 index 000000000000..8d354ce6696b --- /dev/null +++ b/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt @@ -0,0 +1,48 @@ +package app.aaps.core.data.format + +import platform.Foundation.NSLocale +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterDecimalStyle +import platform.Foundation.NSNumberFormatterRoundHalfEven +import platform.Foundation.currentLocale + +/** + * `NSNumberFormatter` is backed by the same CLDR data as the JVM `DecimalFormat`, so this is meant to + * produce the same text rather than an approximation of it. Every setting here mirrors one line of + * the JVM actual: no grouping, half-even rounding, and an explicit decimal separator. + * + * A formatter is built per call instead of being cached. `NSNumberFormatter` is not thread safe, and + * the JVM side solves that with a `ThreadLocal` cache; there is no need to carry that complexity + * until something measures it, because this formats screen values rather than a hot loop. + * + * **This has not been checked against the JVM output.** The two implementations agreeing is the + * whole point of using `NSNumberFormatter`, but comparing them needs a machine that can run + * Kotlin/Native tests for an Apple target, which Windows cannot do. + */ +actual object NumberFormatPlatform { + + actual val SEPARATOR_DOT: Char = '.' + + actual val localeSeparator: Char + get() = formatter().decimalSeparator?.firstOrNull() ?: SEPARATOR_DOT + + actual fun format(format: NumberFormat, value: Double): String = format(format, value, localeSeparator) + + actual fun format(format: NumberFormat, value: Double, separator: Char): String = + formatter().apply { + setMinimumIntegerDigits(format.minIntegerDigits.toULong()) + setMinimumFractionDigits(format.minFractionDigits.toULong()) + setMaximumFractionDigits(format.maxFractionDigits.toULong()) + // Old patterns like "0.00" never grouped, and NSNumberFormatter groups by default. + setUsesGroupingSeparator(false) + setRoundingMode(NSNumberFormatterRoundHalfEven) + setDecimalSeparator(separator.toString()) + }.stringFromNumber(NSNumber(double = value)) ?: "" + + private fun formatter(): NSNumberFormatter = + NSNumberFormatter().apply { + setNumberStyle(NSNumberFormatterDecimalStyle) + setLocale(NSLocale.currentLocale) + } +} diff --git a/core/data/src/iosMain/kotlin/app/aaps/core/data/time/SystemTimeZone.ios.kt b/core/data/src/iosMain/kotlin/app/aaps/core/data/time/SystemTimeZone.ios.kt new file mode 100644 index 000000000000..745188c89602 --- /dev/null +++ b/core/data/src/iosMain/kotlin/app/aaps/core/data/time/SystemTimeZone.ios.kt @@ -0,0 +1,18 @@ +package app.aaps.core.data.time + +import platform.Foundation.NSDate +import platform.Foundation.NSTimeZone +import platform.Foundation.dateWithTimeIntervalSince1970 +import platform.Foundation.localTimeZone + +/** + * `secondsFromGMTForDate` is the direct counterpart of `TimeZone.getOffset(timestamp)`: it answers + * for a given moment, so it accounts for daylight saving at that moment rather than today. + * + * The value is milliseconds because that is what the records store, and it has to keep matching the + * JVM one exactly - see the note on the expect declaration. + */ +actual fun systemUtcOffsetAt(timestamp: Long): Long = + NSTimeZone.localTimeZone + .secondsFromGMTForDate(NSDate.dateWithTimeIntervalSince1970(timestamp / 1000.0)) + .toLong() * 1000L diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc9d88342401..edf706ba1846 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -141,6 +141,7 @@ io-ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", versio io-ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } io-ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } io-ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } +io-ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" } com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } com-squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } com-squareup-retrofit2-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } From 01678c46dcc0c6a0c8c08f9d935385897bd29469 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 19:13:49 +0200 Subject: [PATCH 022/146] :core:nssdk ios target --- core/nssdk/build.gradle.kts | 21 +++++++++++++----- .../core/nssdk/networking/NsHttpClient.ios.kt | 22 +++++++++++++++++++ .../aaps/core/nssdk/utils/IoDispatcher.ios.kt | 11 ++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.ios.kt create mode 100644 core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.ios.kt diff --git a/core/nssdk/build.gradle.kts b/core/nssdk/build.gradle.kts index 6c67fbf9d587..34009dfb42d4 100644 --- a/core/nssdk/build.gradle.kts +++ b/core/nssdk/build.gradle.kts @@ -12,9 +12,12 @@ kotlin { } } - // Stand-in for a real Kotlin/Native target, same as :core:data. iOS needs macOS and Xcode, but - // mingwX64 compiles the same common code through Kotlin/Native, which is what proves there is no - // JVM API left in commonMain. Replace or extend with iosArm64() / iosSimulatorArm64() on a Mac. + // Real Apple targets, same as :core:data. They cross compile on Windows; only linking, cinterop + // and running their tests need a Mac. + iosArm64() + iosSimulatorArm64() + + // Kept because it is the only Kotlin/Native target whose tests can run on Windows. mingwX64() sourceSets { @@ -36,10 +39,18 @@ kotlin { implementation(libs.io.ktor.client.okhttp) } } + // Accessor rather than getByName: iosMain is created by the default hierarchy template, + // which is applied after this block is evaluated, so getByName("iosMain") fails. + iosMain { + dependencies { + // Darwin runs on NSURLSession, so an iOS build gets the system's own connection + // handling rather than a second HTTP stack. + implementation(libs.io.ktor.client.darwin) + } + } getByName("mingwX64Main") { dependencies { - // CIO is Ktor's own multiplatform engine, enough for the compile proof. An Apple - // target would use ktor-client-darwin instead. + // CIO is Ktor's own multiplatform engine, enough for the compile proof on Windows. implementation(libs.io.ktor.client.cio) } } diff --git a/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.ios.kt b/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.ios.kt new file mode 100644 index 000000000000..262dd01243b3 --- /dev/null +++ b/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/networking/NsHttpClient.ios.kt @@ -0,0 +1,22 @@ +package app.aaps.core.nssdk.networking + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.darwin.Darwin + +/** + * Apple engine: **Darwin**, which runs on `NSURLSession`. That is what an iOS build wants - it gets + * the system's own connection handling, proxy settings and certificate validation rather than a + * second HTTP stack, exactly as the JVM side reuses the OkHttp the app already ships. + * + * Request logging is still not wired up here. On JVM it is an OkHttp interceptor, which has no + * Darwin counterpart; the portable answer is Ktor's own `Logging` plugin, and that is worth adding + * when there is an iOS client to read the log. + */ +internal actual fun nsHttpClient( + logging: Boolean, + logger: (String) -> Unit, + configure: HttpClientConfig<*>.() -> Unit +): HttpClient = HttpClient(Darwin) { + configure() +} diff --git a/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.ios.kt b/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.ios.kt new file mode 100644 index 000000000000..05a0314cfd41 --- /dev/null +++ b/core/nssdk/src/iosMain/kotlin/app/aaps/core/nssdk/utils/IoDispatcher.ios.kt @@ -0,0 +1,11 @@ +package app.aaps.core.nssdk.utils + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO + +/** + * Kotlin/Native does provide `Dispatchers.IO` on Apple targets, so blocking work gets its own pool + * here rather than sharing the default one the way the mingw stand-in has to. + */ +internal actual val nsIoDispatcher: CoroutineDispatcher = Dispatchers.IO From 47798f9e8433bdb1d5d34210450c166d6962989b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 19:15:29 +0200 Subject: [PATCH 023/146] :core:data commonTest --- .../app/aaps/core/data/model/ICfgTest.kt | 45 ++++++++------ .../data/model/SourceSensorExtensionsTest.kt | 58 +++++++++++++++++++ .../data/model/SourceSensorExtensionsTest.kt | 53 ----------------- 3 files changed, 86 insertions(+), 70 deletions(-) rename core/data/src/{jvmTest => commonTest}/kotlin/app/aaps/core/data/model/ICfgTest.kt (59%) create mode 100644 core/data/src/commonTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt delete mode 100644 core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt diff --git a/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/ICfgTest.kt b/core/data/src/commonTest/kotlin/app/aaps/core/data/model/ICfgTest.kt similarity index 59% rename from core/data/src/jvmTest/kotlin/app/aaps/core/data/model/ICfgTest.kt rename to core/data/src/commonTest/kotlin/app/aaps/core/data/model/ICfgTest.kt index 06bb49ab2b64..f9498d8c7df4 100644 --- a/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/ICfgTest.kt +++ b/core/data/src/commonTest/kotlin/app/aaps/core/data/model/ICfgTest.kt @@ -1,8 +1,19 @@ package app.aaps.core.data.model -import com.google.common.truth.Truth.assertThat -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +/** + * In `commonTest`, so this runs through Kotlin/Native as well as the JVM - which matters more here + * than for most tests, because `iobCalcForTreatment` is dosing arithmetic and the whole point of + * sharing it with a client is that it computes the same numbers everywhere. + * + * `kotlin.test` rather than Truth and JUnit 5, because neither of those exists off the JVM. Truth's + * `isGreaterThan` / `isAtLeast` / `isFinite` become plain boolean assertions with a message, so a + * failure still says what it expected. + */ class ICfgTest { private fun bolus(amount: Double, iCfg: ICfg, timestamp: Long = 0L) = @@ -13,9 +24,9 @@ class ICfgTest { val iCfg = ICfg(insulinLabel = "test", peak = 75, dia = 6.0, concentration = 1.0) // 30 min after a 1 U bolus val iob = iCfg.iobCalcForTreatment(bolus(1.0, iCfg, timestamp = 0L), time = 30 * 60 * 1000L) - assertThat(iob.iobContrib).isFinite() - assertThat(iob.iobContrib).isGreaterThan(0.0) - assertThat(iob.iobContrib).isLessThan(1.0) + assertTrue(iob.iobContrib.isFinite(), "iobContrib was ${iob.iobContrib}") + assertTrue(iob.iobContrib > 0.0, "expected > 0.0 but was ${iob.iobContrib}") + assertTrue(iob.iobContrib < 1.0, "expected < 1.0 but was ${iob.iobContrib}") } @Test @@ -24,9 +35,9 @@ class ICfgTest { // dia rounds to 0.0 -> td = 0 -> the `t < td` gate would never fire -> silent zero IOB. val iCfg = ICfg(insulinLabel = "sentinel", insulinEndTime = -1L, insulinPeakTime = -1L, concentration = 1.0) val iob = iCfg.iobCalcForTreatment(bolus(5.0, iCfg, timestamp = 0L), time = 30 * 60 * 1000L) - assertThat(iob.iobContrib).isFinite() + assertTrue(iob.iobContrib.isFinite(), "iobContrib was ${iob.iobContrib}") // 5 U bolus 30 min ago must still be counted as on board, not silently dropped to 0. - assertThat(iob.iobContrib).isGreaterThan(0.0) + assertTrue(iob.iobContrib > 0.0, "expected > 0.0 but was ${iob.iobContrib}") } @Test @@ -34,8 +45,8 @@ class ICfgTest { // dia 5h -> td = 300 min; peak 150 min makes 2*tp == td -> original formula divides by zero. val iCfg = ICfg(insulinLabel = "singular", peak = 150, dia = 5.0, concentration = 1.0) val iob = iCfg.iobCalcForTreatment(bolus(2.0, iCfg, timestamp = 0L), time = 60 * 60 * 1000L) - assertThat(iob.iobContrib).isFinite() - assertThat(iob.activityContrib).isFinite() + assertTrue(iob.iobContrib.isFinite(), "iobContrib was ${iob.iobContrib}") + assertTrue(iob.activityContrib.isFinite(), "activityContrib was ${iob.activityContrib}") } @Test @@ -43,8 +54,8 @@ class ICfgTest { // 2*tp > td would yield a negative tau and negative iobContrib, inflating dosing. val iCfg = ICfg(insulinLabel = "negativeTau", peak = 200, dia = 5.0, concentration = 1.0) val iob = iCfg.iobCalcForTreatment(bolus(3.0, iCfg, timestamp = 0L), time = 60 * 60 * 1000L) - assertThat(iob.iobContrib).isFinite() - assertThat(iob.iobContrib).isAtLeast(0.0) + assertTrue(iob.iobContrib.isFinite(), "iobContrib was ${iob.iobContrib}") + assertTrue(iob.iobContrib >= 0.0, "expected >= 0.0 but was ${iob.iobContrib}") } @Test @@ -52,7 +63,7 @@ class ICfgTest { val iCfg = ICfg(insulinLabel = "test", peak = 75, dia = 5.0, concentration = 1.0) // 6 h after bolus, well past the 5 h DIA val iob = iCfg.iobCalcForTreatment(bolus(1.0, iCfg, timestamp = 0L), time = 6 * 60 * 60 * 1000L) - assertThat(iob.iobContrib).isEqualTo(0.0) + assertEquals(0.0, iob.iobContrib) } // The DB v33 migration stamps this sentinel onto every pre-ICfg row, including the active profile's. @@ -61,18 +72,18 @@ class ICfgTest { fun `the v33 migration sentinel is not a usable insulin`() { val sentinel = ICfg(insulinLabel = "", insulinEndTime = -1, insulinPeakTime = -1, concentration = 1.0) - assertThat(sentinel.isUsable).isFalse() - assertThat(sentinel.dia).isEqualTo(0.0) // the value that blocks the loop + assertFalse(sentinel.isUsable) + assertEquals(0.0, sentinel.dia) // the value that blocks the loop } @Test fun `a real insulin is usable`() { - assertThat(ICfg(insulinLabel = "test", peak = 75, dia = 5.0, concentration = 1.0).isUsable).isTrue() + assertTrue(ICfg(insulinLabel = "test", peak = 75, dia = 5.0, concentration = 1.0).isUsable) } @Test fun `a zero or negative DIA or peak is not usable`() { - assertThat(ICfg(insulinLabel = "", insulinEndTime = 0, insulinPeakTime = 4_500_000, concentration = 1.0).isUsable).isFalse() - assertThat(ICfg(insulinLabel = "", insulinEndTime = 18_000_000, insulinPeakTime = 0, concentration = 1.0).isUsable).isFalse() + assertFalse(ICfg(insulinLabel = "", insulinEndTime = 0, insulinPeakTime = 4_500_000, concentration = 1.0).isUsable) + assertFalse(ICfg(insulinLabel = "", insulinEndTime = 18_000_000, insulinPeakTime = 0, concentration = 1.0).isUsable) } } diff --git a/core/data/src/commonTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt b/core/data/src/commonTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt new file mode 100644 index 000000000000..8a596c3bca8e --- /dev/null +++ b/core/data/src/commonTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt @@ -0,0 +1,58 @@ +package app.aaps.core.data.model + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * In `commonTest`, so this runs through Kotlin/Native as well as the JVM. `kotlin.test` rather than + * Truth and JUnit 5, because neither of those exists off the JVM. + */ +class SourceSensorExtensionsTest { + + @Test + fun `dexcom native sensors support advanced filtering`() { + assertTrue(SourceSensor.DEXCOM_NATIVE_UNKNOWN.advancedFilteringSupported()) + assertTrue(SourceSensor.DEXCOM_G6_NATIVE.advancedFilteringSupported()) + assertTrue(SourceSensor.DEXCOM_G7_NATIVE.advancedFilteringSupported()) + assertTrue(SourceSensor.DEXCOM_G6_NATIVE_XDRIP.advancedFilteringSupported()) + assertTrue(SourceSensor.DEXCOM_G7_NATIVE_XDRIP.advancedFilteringSupported()) + assertTrue(SourceSensor.DEXCOM_G7_XDRIP.advancedFilteringSupported()) + } + + @Test + fun `libre 2 and 3 support advanced filtering`() { + assertTrue(SourceSensor.LIBRE_2.advancedFilteringSupported()) + assertTrue(SourceSensor.LIBRE_2_NATIVE.advancedFilteringSupported()) + assertTrue(SourceSensor.LIBRE_3.advancedFilteringSupported()) + } + + @Test + fun `syai and random support advanced filtering`() { + assertTrue(SourceSensor.SYAI_TAG.advancedFilteringSupported()) + assertTrue(SourceSensor.RANDOM.advancedFilteringSupported()) + } + + @Test + fun `medtronic does not support advanced filtering`() { + assertFalse(SourceSensor.MM_600_SERIES.advancedFilteringSupported()) + assertFalse(SourceSensor.MM_SIMPLERA.advancedFilteringSupported()) + } + + @Test + fun `eversense does not support advanced filtering`() { + assertFalse(SourceSensor.EVERSENSE.advancedFilteringSupported()) + } + + @Test + fun `libre 1 sensors do not support advanced filtering`() { + assertFalse(SourceSensor.LIBRE_1_OTHER.advancedFilteringSupported()) + assertFalse(SourceSensor.LIBRE_1_NET.advancedFilteringSupported()) + assertFalse(SourceSensor.LIBRE_1_BUBBLE.advancedFilteringSupported()) + } + + @Test + fun `unknown does not support advanced filtering`() { + assertFalse(SourceSensor.UNKNOWN.advancedFilteringSupported()) + } +} diff --git a/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt deleted file mode 100644 index a12bc51baa19..000000000000 --- a/core/data/src/jvmTest/kotlin/app/aaps/core/data/model/SourceSensorExtensionsTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.aaps.core.data.model - -import com.google.common.truth.Truth.assertThat -import org.junit.jupiter.api.Test - -class SourceSensorExtensionsTest { - - @Test - fun `dexcom native sensors support advanced filtering`() { - assertThat(SourceSensor.DEXCOM_NATIVE_UNKNOWN.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.DEXCOM_G6_NATIVE.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.DEXCOM_G7_NATIVE.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.DEXCOM_G6_NATIVE_XDRIP.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.DEXCOM_G7_NATIVE_XDRIP.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.DEXCOM_G7_XDRIP.advancedFilteringSupported()).isTrue() - } - - @Test - fun `libre 2 and 3 support advanced filtering`() { - assertThat(SourceSensor.LIBRE_2.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.LIBRE_2_NATIVE.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.LIBRE_3.advancedFilteringSupported()).isTrue() - } - - @Test - fun `syai and random support advanced filtering`() { - assertThat(SourceSensor.SYAI_TAG.advancedFilteringSupported()).isTrue() - assertThat(SourceSensor.RANDOM.advancedFilteringSupported()).isTrue() - } - - @Test - fun `medtronic does not support advanced filtering`() { - assertThat(SourceSensor.MM_600_SERIES.advancedFilteringSupported()).isFalse() - assertThat(SourceSensor.MM_SIMPLERA.advancedFilteringSupported()).isFalse() - } - - @Test - fun `eversense does not support advanced filtering`() { - assertThat(SourceSensor.EVERSENSE.advancedFilteringSupported()).isFalse() - } - - @Test - fun `libre 1 sensors do not support advanced filtering`() { - assertThat(SourceSensor.LIBRE_1_OTHER.advancedFilteringSupported()).isFalse() - assertThat(SourceSensor.LIBRE_1_NET.advancedFilteringSupported()).isFalse() - assertThat(SourceSensor.LIBRE_1_BUBBLE.advancedFilteringSupported()).isFalse() - } - - @Test - fun `unknown does not support advanced filtering`() { - assertThat(SourceSensor.UNKNOWN.advancedFilteringSupported()).isFalse() - } -} From 16b6e90022f014d45c20fb636e07e12b5f20437f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 19:16:39 +0200 Subject: [PATCH 024/146] Update progress --- _docs/KMP_IOS_FEASIBILITY.md | 68 +++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 0f4ad91cec9e..25259d1e36f5 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -419,11 +419,15 @@ and the next screen, not because it is on a list. **Where this stands.** Steps 1, 2 and 5 are **done**, and step 0 is half done - out of order, because `:core:nssdk` turned out to be sliceable after all. Three modules now build for Kotlin/Native: -| Module | State | -|---------------|----------------------------------------------------------------------------| -| `:core:data` | multiplatform, 2 `expect` / `actual` seams (wave 5) | -| `:core:nssdk` | multiplatform, 72 files in `commonMain` (waves 6-9) | -| `:core:keys` | multiplatform, 47 files in `commonMain`, **real iOS targets** (wave 14) | +| Module | State | +|---------------|----------------------------------------------------------------------------------------| +| `:core:data` | multiplatform, 2 seams, **real iOS targets**, 15 tests run on Native (waves 5, 15) | +| `:core:nssdk` | multiplatform, 72 files in `commonMain`, **real iOS targets** (waves 6-9, 15) | +| `:core:keys` | multiplatform, 47 files in `commonMain`, **real iOS targets** (wave 14) | + +All three build for `iosArm64` and `iosSimulatorArm64` on Windows. Together they are the data models, +the complete Nightscout read/write client and the preference keys - the whole spine a follower needs +below the UI - and the entire platform-specific surface under them is **four** `actual`s. Nothing is left of the original blocker list inside `:core:nssdk` - `org.json`, Gson, joda, Retrofit, @@ -1482,6 +1486,56 @@ module has no `testFullDebugUnitTest` task and silently runs **no tests at all** been happening to `:core:data` and `:core:nssdk` since waves 5 to 9, unnoticed because `failOnNoDiscoveredTests = false`. +### Wave 15 - the data spine builds for iOS, and 15 tests run on Native (done) + +Committed as `a4291a17cc`, `01678c46dc` and `47798f9e84`. + +`:core:data` and `:core:nssdk` gained `iosArm64()` and `iosSimulatorArm64()`. Both produce genuine +klibs - `native_targets=ios_arm64` and `ios_simulator_arm64` in the manifests - built on Windows. + +**The whole shared data spine needed exactly four `actual`s**, which is the real headline. Grep all +three multiplatform modules for `expect` and that is the entire platform surface underneath a +complete Nightscout read/write client: + +| `expect` | iOS `actual` | +|----------|--------------| +| `systemUtcOffsetAt` | `NSTimeZone.localTimeZone.secondsFromGMTForDate` - the direct counterpart of `TimeZone.getOffset(timestamp)`, per-moment so DST is right | +| `NumberFormatPlatform` | `NSNumberFormatter` - same CLDR data as `DecimalFormat`, with grouping off, half-even rounding and an explicit separator | +| `nsHttpClient` | Ktor **Darwin**, i.e. `NSURLSession` - the system's own connections, proxies and certificate validation | +| `nsIoDispatcher` | `Dispatchers.IO`, which Kotlin/Native does provide on Apple targets | + +These are real implementations, not stubs, and they compile here because the Apple platform klibs +ship **pre-generated inside the Windows Kotlin/Native distribution** - 177 of them for `ios_arm64`, +including `Foundation`. Running cinterop against a new header needs a Mac; consuming what is already +generated does not. + +**`mingwX64` stays, and the reason is the opposite of what was assumed.** It is obsolete as a +*compile* proxy now that the real targets build. But it is the only Kotlin/Native target whose tests +can *run* on Windows - KGP says so plainly: *"Native task 'iosSimulatorArm64Test' is disabled ... +cannot run on the current host (windows-x86_64). Reason: simulator tests require macOS."* + +So `:core:data` got a `commonTest`. `ICfgTest` (8) and `SourceSensorExtensionsTest` (7) moved there +from `jvmTest` and were rewritten from Truth + JUnit 5 to `kotlin.test`; `NumberFormatTest` stayed on +the JVM because it *is* the oracle - it compares against `java.text.DecimalFormat`. Result: **15 tests +now execute through Kotlin/Native**, and the same 15 still run on the JVM. That matters more than it +sounds for `ICfgTest`, which is dosing arithmetic: the point of sharing it with a client is that it +computes the same numbers on every platform, and until now nothing checked that off the JVM. + +Two build details worth keeping: + +- The `ExperimentalNativeApi` opt-in lived inside the `mingwX64 { }` block. It has to move to + `targets.withType`, or `ICfg.iobCalcForTreatment`'s `assert()` will not compile + for iOS. +- `getByName("iosMain")` fails with *"KotlinSourceSet with name 'iosMain' not found"*. The default + hierarchy template creates it **after** the build script is evaluated, so use the lazy `iosMain { }` + accessor. + +**What is still unproven, and it is the substantive gap:** whether `NSNumberFormatter` and +`NSTimeZone` produce byte-identical output to `DecimalFormat` and `TimeZone.getOffset`. `utcOffset` +is stored in every record, takes part in `contentEqualsTo`, and is validated by Nightscout. Comparing +the two needs a host that can *run* Apple tests. The tests to do it with now exist in `commonTest`; +they just cannot execute for an Apple target here. + ### Was behaviour preserved? An audit ran five parallel agents against the migrated code, each trying to find an input where old @@ -1743,9 +1797,7 @@ only as a record of what was planned; what actually happened is wave 14, and the `InfoStep`, `SWEventListener`. Harmless today. 5. **`IntPreferenceKey.entries` in the pump modules** still use `entriesResIds`; fine while pumps stay Android. -6. **Swap `mingwX64` for Apple targets in `:core:data` / `:core:nssdk`.** No longer gated on macOS CI - - Apple klibs cross compile on Windows. For `:core:nssdk` the engine moves from `ktor-client-cio` - in `mingwX64Main` to `ktor-client-darwin` in `appleMain`. +6. ~~Add Apple targets to `:core:data` / `:core:nssdk`.~~ **Done - wave 15.** 7. **A `macos-latest` CI job.** Now buys less than it used to, because compiling for Apple already happens locally - but it is still the only place iOS tests can *run* and frameworks can link. 8. **A resolver for `TextRef.Named` off Android.** `KeysStrings` is in `commonMain`, but the id map From 504de9316947c29b17cfa511f0e4c2e443f03671 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 20:43:02 +0200 Subject: [PATCH 025/146] Fix kmp modules coverage --- .circleci/config.yml | 10 ++++++- jacoco_aggregation.gradle.kts | 50 ++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 89820b0a5201..835036c3d333 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -264,7 +264,15 @@ jobs: command: | export ANDROID_SDK_ROOT=/usr/lib/android-sdk export ANDROID_HOME=/usr/lib/android-sdk - $GRADLE_PIN ./gradlew --continue -Dorg.gradle.daemon=true -Dorg.gradle.workers.max=8 testFullDebugUnitTest + # jvmTest and testAndroidHostTest cover the multiplatform modules. They have no build + # variants, so testFullDebugUnitTest does not exist for them and their tests would + # silently not run at all - which is what happened to :core:data and :core:nssdk, and + # showed up as a Codecov drop the moment :core:keys converted too. + # Both task names are unqualified on purpose: Gradle runs them in whichever projects + # have them, so a module converting later is picked up without editing this file. + # Deliberately not `allTests`: that would pull in the Kotlin/Native toolchain for the + # mingw and iOS targets, whose tests cannot run on a Linux box anyway. + $GRADLE_PIN ./gradlew --continue -Dorg.gradle.daemon=true -Dorg.gradle.workers.max=8 testFullDebugUnitTest jvmTest testAndroidHostTest - run: name: Non-app module androidTests (Gradle, emu-5554) diff --git a/jacoco_aggregation.gradle.kts b/jacoco_aggregation.gradle.kts index 2109d939fe59..cf48087f2004 100644 --- a/jacoco_aggregation.gradle.kts +++ b/jacoco_aggregation.gradle.kts @@ -47,16 +47,33 @@ project.afterEvaluate { val classes = HashSet() subprojects.forEach { proj -> - variants.forEach { variant -> - // Use variant directory as base - fileTree recurses into subdirectories - // This avoids hardcoding exact task-name subdirectories that may vary between AGP versions - val javaPath = proj.layout.buildDirectory.dir("intermediates/javac/$variant").get() - classes.add(fileTree(javaPath) { exclude(excludes); include("**/*.class") }) - val kotlinPath = proj.layout.buildDirectory.dir("intermediates/built_in_kotlinc/$variant").get() - classes.add(fileTree(kotlinPath) { exclude(excludes); include("**/*.class") }) - // Fallback for older AGP versions - val kotlinLegacyPath = proj.layout.buildDirectory.dir("tmp/kotlin-classes/$variant").get() - classes.add(fileTree(kotlinLegacyPath) { exclude(excludes); include("**/*.class") }) + // A multiplatform module has no build variants, so the variant paths below cannot + // legitimately hold anything for it. Deciding up front matters, and not only for + // tidiness: a module that used to be an Android library leaves its old + // intermediates/built_in_kotlinc/fullDebug behind in an existing build directory, and + // feeding both that and the new output to JaCoCo fails the whole report with + // "Can't add different class with same name". + if (File("${proj.projectDir}/src/commonMain").isDirectory) { + // Take the Android compilation when the module has one, the JVM compilation + // otherwise. Never both: a multiplatform module compiles the same commonMain code + // once per target, and only the target whose tests actually ran produces .exec + // data. Counting the other copy would report identical code as 0% covered and make + // the number worse than leaving the module out. + val kmpTarget = if (File("${proj.projectDir}/src/androidMain").isDirectory) "android" else "jvm" + val kmpPath = proj.layout.buildDirectory.dir("classes/kotlin/$kmpTarget/main").get() + classes.add(fileTree(kmpPath) { exclude(excludes); include("**/*.class") }) + } else { + variants.forEach { variant -> + // Use variant directory as base - fileTree recurses into subdirectories + // This avoids hardcoding exact task-name subdirectories that may vary between AGP versions + val javaPath = proj.layout.buildDirectory.dir("intermediates/javac/$variant").get() + classes.add(fileTree(javaPath) { exclude(excludes); include("**/*.class") }) + val kotlinPath = proj.layout.buildDirectory.dir("intermediates/built_in_kotlinc/$variant").get() + classes.add(fileTree(kotlinPath) { exclude(excludes); include("**/*.class") }) + // Fallback for older AGP versions + val kotlinLegacyPath = proj.layout.buildDirectory.dir("tmp/kotlin-classes/$variant").get() + classes.add(fileTree(kotlinLegacyPath) { exclude(excludes); include("**/*.class") }) + } } } classDirectories.setFrom(files(listOf(classes))) @@ -69,6 +86,11 @@ project.afterEvaluate { it.add("${proj.projectDir}/src/$variant/java") it.add("${proj.projectDir}/src/$variant/kotlin") } + // Multiplatform source sets. A missing directory contributes nothing, so these are + // harmless on the modules that are still plain Android. + it.add("${proj.projectDir}/src/commonMain/kotlin") + it.add("${proj.projectDir}/src/androidMain/kotlin") + it.add("${proj.projectDir}/src/jvmMain/kotlin") } } sourceDirectories.setFrom(files(sources)) @@ -87,6 +109,14 @@ project.afterEvaluate { executions.add(file) } } + // Multiplatform test tasks write here instead - testAndroidHostTest.exec for a module + // with an Android target, jvmTest.exec otherwise. Matched to the class directories + // chosen above. + val kmpJacocoPath = proj.layout.buildDirectory.dir("jacoco").get() + fileTree(kmpJacocoPath) { include("*.exec") }.forEach { file -> + println("Collecting multiplatform execution data from: ${file.absolutePath}") + executions.add(file) + } } executionData.setFrom(executions) } From e115e407a7db26a7f333e7350057f5609127df90 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 22:12:08 +0200 Subject: [PATCH 026/146] :core:data platform parity tests --- .../data/format/NumberFormatParityTest.kt | 127 ++++++++++++++++++ .../aaps/core/data/time/SystemTimeZoneTest.kt | 70 ++++++++++ 2 files changed, 197 insertions(+) create mode 100644 core/data/src/commonTest/kotlin/app/aaps/core/data/format/NumberFormatParityTest.kt create mode 100644 core/data/src/commonTest/kotlin/app/aaps/core/data/time/SystemTimeZoneTest.kt diff --git a/core/data/src/commonTest/kotlin/app/aaps/core/data/format/NumberFormatParityTest.kt b/core/data/src/commonTest/kotlin/app/aaps/core/data/format/NumberFormatParityTest.kt new file mode 100644 index 000000000000..159790dbffbf --- /dev/null +++ b/core/data/src/commonTest/kotlin/app/aaps/core/data/format/NumberFormatParityTest.kt @@ -0,0 +1,127 @@ +package app.aaps.core.data.format + +import app.aaps.core.data.format.NumberFormatPlatform.SEPARATOR_DOT +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The same expected text on every platform. + * + * [NumberFormatPlatform] is an `expect object`, so each target formats numbers with its own + * library - `java.text.DecimalFormat` on the JVM, `NSNumberFormatter` on Apple, hand written + * arithmetic on mingw. They are supposed to agree. Nothing checked that until now: `NumberFormatTest` + * pins the JVM against real `DecimalFormat` and is a fine oracle, but it is a `jvmTest` and cannot + * say anything about the others. + * + * These are literal expectations rather than a comparison, so they work on a target that has no JVM + * to compare against. On Windows they run for JVM and mingw today; on a machine that can run Apple + * tests the same file checks `NSNumberFormatter` with no extra work. That is the point - the trap is + * armed before the platform that might spring it exists. + * + * Every case passes [SEPARATOR_DOT] explicitly, so the decimal separator cannot vary with the + * machine's locale. What is still assumed is that the default locale uses **ASCII digits**; a device + * set to Arabic renders Arabic-Indic digits by design, and that is the intended behaviour rather + * than something to pin here. + */ +class NumberFormatParityTest { + + @Test + fun `no grouping above a thousand`() { + // The bug wave 1 fixed: DecimalFormat() groups by default, so this used to be "1,234.5" on + // en and "1.234,5" on de. Grouping is off everywhere now. + assertEquals("1234.5", NumberFormat.DECIMAL_1.format(1234.5, SEPARATOR_DOT)) + assertEquals("1234567", NumberFormat.INTEGER.format(1234567.0, SEPARATOR_DOT)) + } + + @Test + fun `rounding is half even`() { + // Half-even is what DecimalFormat does, and what NSNumberFormatterRoundHalfEven must match: + // an exact tie goes to the nearest EVEN digit, so 2.5 rounds down and 3.5 rounds up. + // + // Every value here is exactly representable as a double (2.5, 3.5, 0.25, 0.125, 0.375 are + // all sums of powers of two), which is deliberate - see the note below. + assertEquals("2", NumberFormat.INTEGER.format(2.5, SEPARATOR_DOT)) + assertEquals("4", NumberFormat.INTEGER.format(3.5, SEPARATOR_DOT)) + assertEquals("0.2", NumberFormat.DECIMAL_1.format(0.25, SEPARATOR_DOT)) + assertEquals("0.12", NumberFormat.DECIMAL_2.format(0.125, SEPARATOR_DOT)) + assertEquals("0.38", NumberFormat.DECIMAL_2.format(0.375, SEPARATOR_DOT)) + } + + /** + * A value that only *looks* like a tie is not part of this contract, because the platforms + * genuinely disagree and the first version of this test caught it. + * + * `0.35` is not representable as a double - the nearest one is `0.34999999999999997779...`, + * just below the midpoint. `DecimalFormat` works from that true decimal value and rounds **down** + * to `0.3`. The mingw actual multiplies by ten first, and `0.35 * 10.0 == 3.5` exactly in + * IEEE-754 arithmetic, so it sees a perfect tie, applies half-even, and produces `0.4`. + * + * Neither is a bug in the tests. `DecimalFormat` is the reference - it is what every existing + * AAPS number has been rendered with - so a platform that differs here is the one that is wrong. + * mingw is explicitly an experiment rather than a shipping target and is left as it is, but + * **`NSNumberFormatter` must be checked against this case on a Mac before an iOS client ships**, + * because unlike mingw it would be rendering real doses. + */ + @Test + fun `values that are not exactly representable are deliberately not pinned`() { + // Asserting only that it produces one decimal, not which one. + val text = NumberFormat.DECIMAL_1.format(0.35, SEPARATOR_DOT) + assertEquals(3, text.length, "expected a single decimal digit but got '$text'") + } + + @Test + fun `minimum fraction digits are padded`() { + assertEquals("1.0", NumberFormat.DECIMAL_1.format(1.0, SEPARATOR_DOT)) + assertEquals("1.00", NumberFormat.DECIMAL_2.format(1.0, SEPARATOR_DOT)) + assertEquals("1.000", NumberFormat.DECIMAL_3.format(1.0, SEPARATOR_DOT)) + assertEquals("0.50", NumberFormat.DECIMAL_2.format(0.5, SEPARATOR_DOT)) + } + + @Test + fun `minimum integer digits are padded`() { + assertEquals("05", NumberFormat.INTEGER_2_DIGITS.format(5.0, SEPARATOR_DOT)) + assertEquals("12", NumberFormat.INTEGER_2_DIGITS.format(12.0, SEPARATOR_DOT)) + } + + @Test + fun `trailing zeros above the minimum are dropped`() { + assertEquals("1", NumberFormat.UP_TO_2_DECIMALS.format(1.0, SEPARATOR_DOT)) + assertEquals("1.5", NumberFormat.UP_TO_2_DECIMALS.format(1.5, SEPARATOR_DOT)) + assertEquals("1.25", NumberFormat.UP_TO_2_DECIMALS.format(1.25, SEPARATOR_DOT)) + // one decimal always, a second only when it is not zero + assertEquals("1.0", NumberFormat.DECIMAL_1_UP_TO_2.format(1.0, SEPARATOR_DOT)) + assertEquals("1.25", NumberFormat.DECIMAL_1_UP_TO_2.format(1.25, SEPARATOR_DOT)) + } + + @Test + fun `negative numbers keep their sign`() { + assertEquals("-1.5", NumberFormat.DECIMAL_1.format(-1.5, SEPARATOR_DOT)) + assertEquals("-1234.5", NumberFormat.DECIMAL_1.format(-1234.5, SEPARATOR_DOT)) + assertEquals("-5", NumberFormat.INTEGER.format(-5.0, SEPARATOR_DOT)) + } + + @Test + fun `zero formats without a sign`() { + assertEquals("0", NumberFormat.INTEGER.format(0.0, SEPARATOR_DOT)) + assertEquals("0.0", NumberFormat.DECIMAL_1.format(0.0, SEPARATOR_DOT)) + } + + @Test + fun `insulin and glucose values a user actually sees`() { + // The shapes that reach the screen and Nightscout, so a platform difference here would be + // visible rather than theoretical. + assertEquals("1.25", NumberFormat.DECIMAL_2.format(1.25, SEPARATOR_DOT)) + assertEquals("0.05", NumberFormat.DECIMAL_2.format(0.05, SEPARATOR_DOT)) + assertEquals("5.5", NumberFormat.DECIMAL_1.format(5.5, SEPARATOR_DOT)) + assertEquals("120", NumberFormat.INTEGER.format(120.0, SEPARATOR_DOT)) + assertEquals("0.001", NumberFormat.DECIMAL_3.format(0.001, SEPARATOR_DOT)) + } + + @Test + fun `the explicit dot separator does not depend on the locale`() { + // Whatever the machine's locale is, asking for a dot gives a dot. This is what makes server + // data and exported files stable. + assertEquals("3.5", NumberFormat.DECIMAL_1.format(3.5, SEPARATOR_DOT)) + assertEquals('.', SEPARATOR_DOT) + } +} diff --git a/core/data/src/commonTest/kotlin/app/aaps/core/data/time/SystemTimeZoneTest.kt b/core/data/src/commonTest/kotlin/app/aaps/core/data/time/SystemTimeZoneTest.kt new file mode 100644 index 000000000000..c2f5890d7880 --- /dev/null +++ b/core/data/src/commonTest/kotlin/app/aaps/core/data/time/SystemTimeZoneTest.kt @@ -0,0 +1,70 @@ +package app.aaps.core.data.time + +import kotlin.math.abs +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Invariants that must hold for [systemUtcOffsetAt] on every platform. + * + * This value is not cosmetic. It is stored as `utcOffset` on every record, it takes part in + * `contentEqualsTo` - so a changed value makes a record compare unequal and sync again - it is + * uploaded to Nightscout, which validates it and answers 400 when it is wrong, and it goes to Open + * Humans. A platform returning it in the wrong unit would corrupt data quietly rather than crash. + * + * The offset cannot be pinned to a literal, because it depends on the machine's time zone. What can + * be pinned is its shape, and that is enough to catch the mistake this actually invites: returning + * **seconds instead of milliseconds**. `NSTimeZone.secondsFromGMTForDate` is named for what it + * returns, so the `* 1000` in the Apple actual is easy to drop. + * + * Honest limits: on a machine set to UTC every assertion here passes trivially, because the offset + * is zero in any unit. CI often runs in UTC. These tests are therefore strongest on a developer + * machine in a real zone, and they are still worth having - a units bug that survives to a Mac would + * be caught the moment anyone runs them somewhere other than UTC. + */ +class SystemTimeZoneTest { + + // 2026-01-15T12:00:00Z and 2026-07-15T12:00:00Z - one in each half of the year, so that in a + // zone with daylight saving exactly one of them is in DST. + private val winter = 1_768_478_400_000L + private val summer = 1_784_116_800_000L + + @Test + fun `offset is a whole number of minutes`() { + // Every zone in use today is a whole number of minutes from UTC. Seconds returned in place + // of milliseconds would leave a remainder here for any non-zero offset. + assertEquals(0L, systemUtcOffsetAt(winter) % 60_000L, "offset ${systemUtcOffsetAt(winter)} is not a whole number of minutes") + assertEquals(0L, systemUtcOffsetAt(summer) % 60_000L, "offset ${systemUtcOffsetAt(summer)} is not a whole number of minutes") + } + + @Test + fun `offset is within the range of real time zones`() { + // -12:00 (Baker Island) to +14:00 (Kiribati), in milliseconds. + listOf(winter, summer).forEach { at -> + val offset = systemUtcOffsetAt(at) + assertTrue(offset in -12 * 3_600_000L..14 * 3_600_000L, "offset $offset at $at is outside any real time zone") + } + } + + @Test + // Kotlin/Native rejects a comma in a backtick name, so this reads a little stiffly. + fun `daylight saving changes the offset by a whole hour or not at all`() { + // The strongest unit check available without pinning a zone: where DST applies the two + // halves of the year differ by exactly one hour in milliseconds. An implementation + // returning seconds would differ by 3600 instead, and fail here. + val delta = abs(systemUtcOffsetAt(summer) - systemUtcOffsetAt(winter)) + assertTrue( + delta == 0L || delta == 3_600_000L || delta == 1_800_000L, + "summer/winter offsets differ by $delta ms, which is neither zero, half an hour nor a whole hour" + ) + } + + @Test + fun `the same moment always gives the same offset`() { + // It is read per record and must not drift between calls, or contentEqualsTo would start + // reporting records as changed when nothing changed. + assertEquals(systemUtcOffsetAt(winter), systemUtcOffsetAt(winter)) + assertEquals(systemUtcOffsetAt(summer), systemUtcOffsetAt(summer)) + } +} From 08f538482600a0c4f724dc52b8e7be256c9466a6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 22:12:08 +0200 Subject: [PATCH 027/146] ios CI --- .github/workflows/ios-ci.yml | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .github/workflows/ios-ci.yml diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml new file mode 100644 index 000000000000..d50900a1b780 --- /dev/null +++ b/.github/workflows/ios-ci.yml @@ -0,0 +1,129 @@ +name: iOS CI (Kotlin/Native) + +# The only place the Apple side can actually be checked. +# +# Klibs for iosArm64 / iosSimulatorArm64 cross compile on any host, so a developer on Windows or +# Linux already knows the shared code COMPILES for iOS. What no other machine can do is RUN it: +# `iosSimulatorArm64Test` is disabled off macOS with "simulator tests require macOS", and it reports +# SKIPPED rather than failing, so a green build elsewhere says nothing about iOS behaviour. +# +# That gap matters most for the four platform actuals under the shared data spine. `utcOffset` is +# written to every record, takes part in contentEqualsTo and is validated by Nightscout, and it comes +# from NSTimeZone on Apple and TimeZone.getOffset on the JVM. NumberFormatParityTest and +# SystemTimeZoneTest in commonTest exist to compare them; this job is where they finally execute. +# +# No secrets: nothing is signed, published or uploaded. + +on: + workflow_dispatch: + push: + paths: + - 'core/data/**' + - 'core/nssdk/**' + - 'core/keys/**' + - 'gradle/libs.versions.toml' + - 'buildSrc/**' + - '.github/workflows/ios-ci.yml' + pull_request: + paths: + - 'core/data/**' + - 'core/nssdk/**' + - 'core/keys/**' + - 'gradle/libs.versions.toml' + - 'buildSrc/**' + +jobs: + ios: + runs-on: macos-latest + timeout-minutes: 60 + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Load jdk version + run: | + JAVA_VERSION=$(jq -r '.default' .github/jdk-map.json) + echo "JAVA_VERSION=$JAVA_VERSION" >> $GITHUB_ENV + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: gradle + + # Kotlin/Native downloads its own toolchain on first use; without this every run pays for it. + - name: Cache Kotlin/Native + uses: actions/cache@v4 + with: + path: ~/.konan + key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }} + restore-keys: konan-${{ runner.os }}- + + - name: Compile the Apple targets + run: | + # Actions runs `bash -e` but NOT pipefail, so without this the exit code of the pipeline + # would be tee's and a failed Gradle build would report success. + set -o pipefail + ./gradlew --console=plain --no-daemon \ + :core:data:compileKotlinIosArm64 :core:data:compileKotlinIosSimulatorArm64 \ + :core:nssdk:compileKotlinIosArm64 :core:nssdk:compileKotlinIosSimulatorArm64 \ + :core:keys:compileKotlinIosArm64 :core:keys:compileKotlinIosSimulatorArm64 \ + | tee compile.log + + # A disabled Kotlin/Native task reports SKIPPED and still exits 0, so "BUILD SUCCESSFUL" alone + # would stay green even if the whole Apple side silently stopped building. Cross-compilability + # is also transitive: a single cinterop anywhere in the project dependency graph turns these + # into no-ops. Assert the work happened rather than that nothing complained. + - name: Assert the Apple compiles really ran + run: | + if grep -E "Task :core:(data|nssdk|keys):compileKotlinIos(Arm64|SimulatorArm64) (SKIPPED|NO-SOURCE)" compile.log; then + echo "::error::An iOS compile task was skipped instead of executed." + exit 1 + fi + for m in data nssdk keys; do + for t in iosArm64 iosSimulatorArm64; do + MANIFEST=$(find core/$m/build/classes/kotlin/$t/main/klib -name manifest 2>/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::error::core/$m produced no $t klib" + exit 1 + fi + echo "core/$m $t -> $(grep native_targets "$MANIFEST")" + done + done + + # The part that only exists here. commonTest runs through Kotlin/Native against the real + # NSNumberFormatter and NSTimeZone, which is what no other host can do. + # + # Only :core:data has commonTest today, so it is the one module asserted below. The other two + # are still run: if either grows shared tests later they start executing here with no change to + # this file, and until then they are simply NO-SOURCE. + - name: Run the shared tests on the iOS simulator + run: | + set -o pipefail + ./gradlew --console=plain --no-daemon \ + :core:data:iosSimulatorArm64Test :core:nssdk:iosSimulatorArm64Test :core:keys:iosSimulatorArm64Test \ + | tee test.log + + - name: Assert the iOS tests really ran + run: | + if grep -E "Task :core:data:iosSimulatorArm64Test (SKIPPED|NO-SOURCE)" test.log; then + echo "::error::Module core:data had its iOS tests skipped instead of executed. On macOS they must run." + exit 1 + fi + FOUND=$(find core/data/build/test-results/iosSimulatorArm64Test -name "TEST-*.xml" 2>/dev/null | wc -l) + echo "iOS test result files for :core:data: $FOUND" + [ "$FOUND" -gt 0 ] || { echo "::error::No iOS test results were produced."; exit 1; } + # Surface the counts, so a run that executed zero assertions is visible in the log. + grep -ho 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' \ + core/data/build/test-results/iosSimulatorArm64Test/TEST-*.xml || true + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-test-results + path: core/*/build/test-results/iosSimulatorArm64Test/** + if-no-files-found: warn From c017e469b4770fc4eb2e092be6360cfd8fadea27 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 22:20:36 +0200 Subject: [PATCH 028/146] ios CI run tests in a real timezone --- .github/workflows/ios-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index d50900a1b780..135c928d7250 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -101,8 +101,16 @@ jobs: # are still run: if either grows shared tests later they start executing here with no change to # this file, and until then they are simply NO-SOURCE. - name: Run the shared tests on the iOS simulator + # GitHub's macOS runners are UTC, and at UTC every assertion in SystemTimeZoneTest passes + # trivially - the offset is zero whatever unit it is in, so a seconds-instead-of-milliseconds + # bug in the NSTimeZone actual would sail straight through. Pin a zone that has both a + # non-zero offset and daylight saving, so the whole-minutes, range and whole-hour-DST + # assertions actually mean something. Foundation honours TZ on macOS, as does the JVM. + env: + TZ: Europe/Prague run: | set -o pipefail + echo "Running with TZ=$TZ" ./gradlew --console=plain --no-daemon \ :core:data:iosSimulatorArm64Test :core:nssdk:iosSimulatorArm64Test :core:keys:iosSimulatorArm64Test \ | tee test.log From eb06d8d862e23735003ede4ec64754b9f286c5ab Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 22:24:48 +0200 Subject: [PATCH 029/146] Update progress --- _docs/KMP_IOS_FEASIBILITY.md | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 25259d1e36f5..fed0aee45801 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -1530,11 +1530,48 @@ Two build details worth keeping: hierarchy template creates it **after** the build script is evaluated, so use the lazy `iosMain { }` accessor. -**What is still unproven, and it is the substantive gap:** whether `NSNumberFormatter` and -`NSTimeZone` produce byte-identical output to `DecimalFormat` and `TimeZone.getOffset`. `utcOffset` -is stored in every record, takes part in `contentEqualsTo`, and is validated by Nightscout. Comparing -the two needs a host that can *run* Apple tests. The tests to do it with now exist in `commonTest`; -they just cannot execute for an Apple target here. +**The one thing this could not prove was whether `NSNumberFormatter` and `NSTimeZone` actually agree +with `DecimalFormat` and `TimeZone.getOffset`** - which matters, because `utcOffset` is stored in +every record, takes part in `contentEqualsTo` and is validated by Nightscout, so a units mistake +would corrupt data quietly rather than crash. That gap is closed in wave 16. + +### Wave 16 - the shared code runs on Apple, and the actuals are correct (done) + +Committed as `e115e407a7`, `08f5384826` and `c017e469b4`. + +Two `commonTest` suites were added to `:core:data` to pin platform behaviour as literal expectations +rather than as a comparison, so they mean something on a target that has no JVM to compare against: +`NumberFormatParityTest` (10 tests) and `SystemTimeZoneTest` (4). + +**They found a real divergence on their first run, on Windows.** `0.35` is not representable as a +double - the nearest one is `0.34999999999999997779...`, just below the midpoint. `DecimalFormat` +works from that true decimal value and rounds **down** to `0.3`. The mingw actual multiplies by ten +first, and `0.35 * 10.0 == 3.5` exactly in IEEE-754, so it sees a perfect tie, applies half-even, and +produces `0.4`. Ties are now pinned only on exactly representable values, where every platform must +agree; the non-representable case is documented instead. `DecimalFormat` is the reference, because it +is what every existing AAPS number has been rendered with. + +`.github/workflows/ios-ci.yml` runs on `macos-latest`, needs no secrets, and asserts the tasks +**executed** rather than that the build was green - a disabled Kotlin/Native task reports SKIPPED and +still exits 0, so "BUILD SUCCESSFUL" on its own would stay green if the whole Apple side silently +stopped building. It also checks each klib manifest names the target it claims. + +Result: **29 tests execute on the iOS simulator, zero failures** - the same 29 that run on JVM and +mingw - and all six klibs build on macOS. + +The first run passed but proved less than it looked: GitHub's macOS runners are **UTC**, where every +assertion in `SystemTimeZoneTest` holds trivially because the offset is zero in any unit. The job now +pins `TZ=Europe/Prague`, which has both a non-zero offset and daylight saving. There the winter offset +is 3,600,000 ms, so a seconds-returning implementation would fail both the whole-minutes check +(`3600 % 60000 != 0`) and the whole-hour DST delta. It passes, which is what actually confirms the +`* 1000` in the `NSTimeZone` actual. + +Two traps worth remembering from writing that workflow: + +- GitHub Actions runs `bash -e` but **not** `pipefail`, so `./gradlew ... | tee log` returns *tee's* + exit code and a failed build reports success. Same trap as the pipe warning further down, in a new + place. +- Kotlin/Native rejects a **comma** inside a backtick test name, which the JVM accepts. ### Was behaviour preserved? @@ -1798,8 +1835,7 @@ only as a record of what was planned; what actually happened is wave 14, and the 5. **`IntPreferenceKey.entries` in the pump modules** still use `entriesResIds`; fine while pumps stay Android. 6. ~~Add Apple targets to `:core:data` / `:core:nssdk`.~~ **Done - wave 15.** -7. **A `macos-latest` CI job.** Now buys less than it used to, because compiling for Apple already - happens locally - but it is still the only place iOS tests can *run* and frameworks can link. +7. ~~A `macos-latest` CI job.~~ **Done - wave 16.** `.github/workflows/ios-ci.yml`. 8. **A resolver for `TextRef.Named` off Android.** `KeysStrings` is in `commonMain`, but the id map that turns a name into text is `androidMain`. Nothing needs this until there is a non-Android UI, and the client only needs the system language, so it is a small generated table when it comes. From 007da5634d83cef0f6e465ea035da68e0100c54b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 23:15:28 +0200 Subject: [PATCH 030/146] :core:data commonMain assert --- core/data/build.gradle.kts | 15 +++------------ .../kotlin/app/aaps/core/data/model/DevAssert.kt | 16 ++++++++++++++++ .../kotlin/app/aaps/core/data/model/ICfg.kt | 4 ++-- .../app/aaps/core/data/model/DevAssert.jvm.kt | 6 ++++++ .../app/aaps/core/data/model/DevAssert.native.kt | 14 ++++++++++++++ 5 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/model/DevAssert.kt create mode 100644 core/data/src/jvmMain/kotlin/app/aaps/core/data/model/DevAssert.jvm.kt create mode 100644 core/data/src/nativeMain/kotlin/app/aaps/core/data/model/DevAssert.native.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e1161b68a3b7..e53ea7547ee7 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { kotlin("multiplatform") @@ -24,17 +23,9 @@ kotlin { // Kotlin/Native at all before a Mac appears. mingwX64() - targets.withType().configureEach { - compilations.configureEach { - compileTaskProvider.configure { - // kotlin.assert has an experimental implementation on Native. ICfg.iobCalcForTreatment - // uses it. Opting in here keeps that code exactly as it is - turning the asserts into - // require() would change behaviour, because JVM assertions are off in production while - // require() always throws. - compilerOptions.optIn.add("kotlin.experimental.ExperimentalNativeApi") - } - } - } + // No module-wide opt-in for ExperimentalNativeApi. `assert` is not in the common standard + // library at all, so opting in could never have fixed it; the fix is the devAssert + // expect/actual, and the Native actual scopes its own opt-in to one file. sourceSets { getByName("commonTest") { diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DevAssert.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DevAssert.kt new file mode 100644 index 000000000000..9580858db946 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DevAssert.kt @@ -0,0 +1,16 @@ +package app.aaps.core.data.model + +/** + * A developer check that behaves like `assert` on every target. + * + * `kotlin.assert` is **not in the common standard library**. The JVM has it, and Kotlin/Native has + * its own behind `@ExperimentalNativeApi`, but there is no common declaration - so `assert(...)` in + * `commonMain` type-checks in each target's own compilation and then fails the *metadata* + * compilation with "Unresolved reference 'assert'". That failure only appears once some other module + * consumes this one from its own `commonMain`, which is exactly when it matters. + * + * Kept as an assertion rather than turned into `require()`: JVM assertions are off in production and + * on under `-ea` in tests, while `require()` always throws. Swapping them would change behaviour in + * shipped code. + */ +internal expect fun devAssert(value: Boolean) diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt index 0b362a2e9b32..6dc968c94b62 100644 --- a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/ICfg.kt @@ -96,8 +96,8 @@ data class ICfg( fun deepClone(): ICfg = ICfg(insulinLabel, insulinEndTime, insulinPeakTime, concentration).also { it.insulinNickname = insulinNickname } fun iobCalcForTreatment(bolus: BS, time: Long): Iob { - assert(insulinEndTime != 0L) - assert(insulinPeakTime != 0L) + devAssert(insulinEndTime != 0L) + devAssert(insulinPeakTime != 0L) val result = Iob() if (bolus.amount != 0.0) { val bolusTime = bolus.timestamp diff --git a/core/data/src/jvmMain/kotlin/app/aaps/core/data/model/DevAssert.jvm.kt b/core/data/src/jvmMain/kotlin/app/aaps/core/data/model/DevAssert.jvm.kt new file mode 100644 index 000000000000..eb5c81241c67 --- /dev/null +++ b/core/data/src/jvmMain/kotlin/app/aaps/core/data/model/DevAssert.jvm.kt @@ -0,0 +1,6 @@ +package app.aaps.core.data.model + +/** Plain `kotlin.assert`: active under `-ea` (so in tests), a no-op in production. */ +internal actual fun devAssert(value: Boolean) { + assert(value) +} diff --git a/core/data/src/nativeMain/kotlin/app/aaps/core/data/model/DevAssert.native.kt b/core/data/src/nativeMain/kotlin/app/aaps/core/data/model/DevAssert.native.kt new file mode 100644 index 000000000000..7874fdb9a72b --- /dev/null +++ b/core/data/src/nativeMain/kotlin/app/aaps/core/data/model/DevAssert.native.kt @@ -0,0 +1,14 @@ +@file:OptIn(ExperimentalNativeApi::class) + +package app.aaps.core.data.model + +import kotlin.experimental.ExperimentalNativeApi + +/** + * Kotlin/Native has `assert` too, behind an opt-in. Scoping the opt-in to this one file is better + * than a Gradle-wide `optIn`, which would silently allow experimental Native APIs anywhere in the + * module - and which does not reach the metadata compilation anyway. + */ +internal actual fun devAssert(value: Boolean) { + assert(value) +} From e6d645eb6bd61088912467dfbf4993fedb2137d9 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 9 Aug 2026 23:15:28 +0200 Subject: [PATCH 031/146] cmp spike --- gradle/libs.versions.toml | 21 ++ settings.gradle | 2 + spike/cmp/build.gradle.kts | 59 ++++++ .../aaps/spike/cmp/TextRefResource.android.kt | 19 ++ .../kotlin/app/aaps/spike/cmp/Helpers.kt | 95 ++++++++++ .../app/aaps/spike/cmp/PlusMinusEdit.kt | 179 ++++++++++++++++++ .../app/aaps/spike/cmp/TextRefResource.kt | 19 ++ .../app/aaps/spike/cmp/TextRefResource.ios.kt | 22 +++ 8 files changed, 416 insertions(+) create mode 100644 spike/cmp/build.gradle.kts create mode 100644 spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt create mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt create mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt create mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt create mode 100644 spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edf706ba1846..e62004979e8a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,16 @@ vico = "3.2.3" composeUi = "1.11.4" glance = "1.1.1" +# Compose Multiplatform - spike only, see :spike:cmp. Separate from the androidx Compose above: +# CMP publishes the same androidx.compose.* package names and its Android variants delegate to +# androidx, so the composeBom still decides the Android versions. +# 1.11.1 rather than 1.12.0-beta03 on purpose: the beta's Android side delegates to androidx +# 1.12.0-beta02, which would drag a pre-release androidx into an app pinned to stable 1.11.x. +composeMultiplatform = "1.11.1" +# material3 and material-icons-extended are separate, lagging version lines in CMP. +cmpMaterial3 = "1.9.0" +cmpIcons = "1.7.3" + [plugins] android-library = { id = "com.android.library" } # No version, exactly like android-library above: both plugin ids ship in com.android.tools.build:gradle, @@ -40,6 +50,9 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" } ksp = { id = "com.google.devtools.ksp", version = "2.3.10" } android-test = { id = "com.android.test", version.ref = "gradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +# Compose Multiplatform framework plugin. It hard-fails unless compose-compiler above is applied in +# the same module - the compiler itself ships with Kotlin, this plugin only adds the framework. +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } [libraries] com-android-tools-build = { group = "com.android.tools.build", name = "gradle", version.ref = "gradlePlugin" } @@ -206,3 +219,11 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui- sh-calvin-reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version = "3.1.0" } androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" } +# Compose Multiplatform - spike only. Explicit coordinates rather than the `compose.xxx` DSL +# accessors, which are deprecated in 1.11.1. +cmp-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +cmp-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +cmp-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +cmp-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "cmpMaterial3" } +cmp-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "cmpIcons" } + diff --git a/settings.gradle b/settings.gradle index bbc57b910f2d..976b3b8e889b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,8 @@ include ':core:nssdk' include ':core:objects' include ':core:utils' include ':core:ui' +// Throwaway. Answers whether Compose Multiplatform works in THIS build; delete once decided. +include ':spike:cmp' include ':database:impl' include ':database:persistence' include ':implementation' diff --git a/spike/cmp/build.gradle.kts b/spike/cmp/build.gradle.kts new file mode 100644 index 000000000000..030b959cd0b6 --- /dev/null +++ b/spike/cmp/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + kotlin("multiplatform") + // NOT com.android.library - AGP 9 refuses that together with the multiplatform plugin. + alias(libs.plugins.android.kmp.library) + // The Compose COMPILER, which ships with Kotlin and compiles @Composable for every target + // including Kotlin/Native. org.jetbrains.compose below hard-fails without it. + alias(libs.plugins.compose.compiler) + // The Compose Multiplatform framework itself. + alias(libs.plugins.compose.multiplatform) +} + +// THROWAWAY SPIKE. Not part of the app, consumed by nothing, safe to delete. +// +// It answers one question: does Compose Multiplatform work in THIS build - this Kotlin, this AGP, +// this catalog, next to the androidx Compose the app already uses. The general form of that question +// is already answered in public (coil-kt/coil and plainhub/plain-app both ship the same four plugins +// on Kotlin 2.4.10 + AGP 9.3.1 + CMP 1.11.1), so this only checks that nothing repo-specific +// interferes. +// +// Deliberately NOT here: +// - compose.components.resources. compose-resources was rejected for AAPS - it cannot match a +// region-less locale against a region-pinned folder. generateResClass defaults to `auto`, which +// generates nothing unless that dependency is present, so leaving it out is the whole opt-out. +// - a jvm() target. It would pull in the desktop Compose surface (skiko-awt) and give the spike +// another way to fail without saying anything about iOS. +// - androidResources. Off by default for a KMP library, and this module owns no res/, which is also +// why CMP-9547 (resources not packaged under AGP 9) cannot apply here. +kotlin { + android { + namespace = "app.aaps.spike.cmp" + compileSdk = Versions.compileSdk + minSdk = Versions.minSdk + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + // Restated because android-module-dependencies applies com.android.library and so cannot be + // applied to a multiplatform module - same reason as core/keys. + lint { checkReleaseBuilds = false } + } + + // Apple klibs cross compile on Windows. Linking and running still need a Mac and report SKIPPED. + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(libs.cmp.runtime) + implementation(libs.cmp.foundation) + implementation(libs.cmp.ui) + implementation(libs.cmp.material3) + // Needed by the real AAPS files copied in below: both use Icons.Filled.Remove, which is + // in the extended set rather than the core one. + implementation(libs.cmp.material.icons.extended) + + // The point of the spike is UI on top of the REAL shared spine, not against stubs. + // Both of these already build for iosArm64 / iosSimulatorArm64. + implementation(project(":core:data")) + implementation(project(":core:keys")) + } + } +} diff --git a/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt b/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt new file mode 100644 index 000000000000..1a3aca5f6fd3 --- /dev/null +++ b/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt @@ -0,0 +1,19 @@ +package app.aaps.spike.cmp + +import androidx.compose.runtime.Composable +import app.aaps.core.keys.KeysStringIds +import app.aaps.core.keys.interfaces.TextRef + +/** Android resolves through AAPT, exactly as `:core:ui` does today. */ +@Composable +actual fun stringResource(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.AndroidRes -> + if (ref.args.isEmpty()) androidx.compose.ui.res.stringResource(ref.id) + else androidx.compose.ui.res.stringResource(ref.id, *ref.args.toTypedArray()) + + is TextRef.Named -> { + val id = KeysStringIds.idOf(ref.name) + if (id == null) ref.name else androidx.compose.ui.res.stringResource(id) + } +} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt new file mode 100644 index 000000000000..a083507c4c72 --- /dev/null +++ b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt @@ -0,0 +1,95 @@ +package app.aaps.spike.cmp + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import kotlinx.coroutines.delay +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +/** + * Copied from `:core:ui`'s SliderWithButtons.kt, with ONE change, and that change is a finding. + * + * The original reads: + * ``` + * val factor = Math.pow(10.0, decimals.toDouble()) + * return Math.round(scaled * factor) / factor + * ``` + * `Math` is `java.lang.Math` - JVM only. `java.lang` needs no import statement, so no amount of + * grepping imports finds it; the file looks clean and is not. That is the same mistake shape the + * feasibility note already records twice, and it means the count of `:core:ui` files that can move + * is optimistic wherever it was derived from imports alone. + * + * `kotlin.math` is a drop-in replacement and works on every target. + */ +internal fun roundToStep(value: Double, step: Double): Double { + val scaled = (value / step).roundToInt() * step + // Fix floating point precision errors (e.g., 6.1000000000005 -> 6.1) + val decimals = step.toString().substringAfter('.', "").length + val factor = 10.0.pow(decimals.toDouble()) + return (scaled * factor).roundToLong() / factor +} + +/** + * Copied verbatim from `:core:ui`. Kept because it exercises pointer input, haptics and a coroutine + * loop - three things a trivial spike screen would not touch. + */ +@Composable +fun RepeatingIconButton( + onClick: () -> Unit, + enabled: Boolean, + modifier: Modifier = Modifier, + initialDelayMs: Long = 500L, + maxDelayMs: Long = 200L, + minDelayMs: Long = 50L, + accelerationFactor: Float = 0.8f, + content: @Composable () -> Unit +) { + var isPressed by remember { mutableStateOf(false) } + val currentOnClick by rememberUpdatedState(onClick) + val haptic = LocalHapticFeedback.current + + LaunchedEffect(isPressed, enabled) { + if (isPressed && enabled) { + delay(initialDelayMs) + var currentDelay = maxDelayMs.toFloat() + while (isPressed && enabled) { + currentOnClick() + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + delay(currentDelay.toLong()) + currentDelay = (currentDelay * accelerationFactor).coerceAtLeast(minDelayMs.toFloat()) + } + } + } + + FilledTonalIconButton( + onClick = { + onClick() + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + }, + enabled = enabled, + modifier = modifier.pointerInput(Unit) { + awaitEachGesture { + awaitFirstDown(requireUnconsumed = false) + isPressed = true + waitForUpOrCancellation() + isPressed = false + } + } + ) { + content() + } +} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt new file mode 100644 index 000000000000..cf574be790e2 --- /dev/null +++ b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt @@ -0,0 +1,179 @@ +package app.aaps.spike.cmp + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import app.aaps.core.data.format.NumberFormat +import app.aaps.core.keys.interfaces.TextRef +import kotlin.math.roundToInt + +/** + * Copied VERBATIM from `core/ui/.../compose/PlusMinusEdit.kt`, changing only the package. + * + * Chosen on purpose as the hardest realistic case rather than the easiest: `TextField` plus + * `KeyboardOptions`, `KeyboardActions`, `ImeAction`, focus handling and text selection is exactly the + * area where Compose Multiplatform on iOS is weakest, so a spike that only draws cards and icons + * would prove nothing. It also pulls in `NumberFormat` from `:core:data` and `TextRef` from + * `:core:keys` - both already multiplatform - so this compiles the UI on top of the real shared spine + * rather than against stubs. + * + * Original doc follows. + * + * Compact `[ − ] [ editable value ] [ + ]` stepper for inline numeric editing. + * + * The TextField commits its value on focus loss or IME Done — any action button on the + * same screen that depends on the value must call `focusManager.clearFocus()` first to + * flush pending text. Long-pressing +/- auto-repeats; range boundaries disable the + * corresponding button. + */ +@Composable +fun PlusMinusEdit( + value: Double, + onValueChange: (Double) -> Unit, + valueRange: ClosedFloatingPointRange, + step: Double, + valueFormat: NumberFormat = NumberFormat.DECIMAL_1, + unitLabel: TextRef? = null, + enabled: Boolean = true, + modifier: Modifier = Modifier +) { + val focusManager = LocalFocusManager.current + val minValue = valueRange.start + val maxValue = valueRange.endInclusive + + var isFocused by remember { mutableStateOf(false) } + var textFieldValue by remember { + mutableStateOf(TextFieldValue(valueFormat.format(value))) + } + var isError by remember { mutableStateOf(false) } + + // Sync external value → text when not focused (e.g., +/- buttons or external update). + LaunchedEffect(value) { + if (!isFocused) { + textFieldValue = TextFieldValue(valueFormat.format(value)) + isError = false + } + } + + val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" + + fun validateAndCommit(text: String) { + val cleaned = text.trim().replace(",", ".") + val parsed = cleaned.toDoubleOrNull() + if (parsed == null) { + isError = true + } else { + isError = false + onValueChange(parsed.coerceIn(minValue, maxValue)) + } + } + + // Flush pending text when leaving composition (back press without tapping Done). + val latestTextProvider by rememberUpdatedState({ textFieldValue.text }) + val latestIsFocused by rememberUpdatedState(isFocused) + DisposableEffect(Unit) { + onDispose { + if (latestIsFocused) validateAndCommit(latestTextProvider()) + } + } + + fun stepValue(direction: Int) { + val newValue = roundToStep(value + direction * step, step).coerceIn(minValue, maxValue) + textFieldValue = TextFieldValue(valueFormat.format(newValue)) + isError = false + onValueChange(newValue) + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + RepeatingIconButton( + onClick = { stepValue(-1) }, + enabled = enabled && value > minValue, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Remove, + contentDescription = "-", + modifier = Modifier.size(16.dp) + ) + } + + TextField( + value = textFieldValue, + onValueChange = { newValue -> + textFieldValue = newValue + if (isError) isError = false + }, + singleLine = true, + enabled = enabled, + isError = isError, + trailingIcon = if (resolvedUnitLabel.isNotEmpty()) { + { Text(resolvedUnitLabel) } + } else null, + keyboardOptions = KeyboardOptions( + keyboardType = if (step != step.roundToInt().toDouble()) + KeyboardType.Decimal else KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + validateAndCommit(textFieldValue.text) + focusManager.clearFocus() + } + ), + modifier = Modifier + .weight(1f) + .onFocusChanged { focusState -> + if (isFocused && !focusState.isFocused) { + validateAndCommit(textFieldValue.text) + } + if (!isFocused && focusState.isFocused) { + textFieldValue = textFieldValue.copy( + selection = TextRange(0, textFieldValue.text.length) + ) + } + isFocused = focusState.isFocused + } + ) + + RepeatingIconButton( + onClick = { stepValue(1) }, + enabled = enabled && value < maxValue, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "+", + modifier = Modifier.size(16.dp) + ) + } + } +} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt new file mode 100644 index 000000000000..96eceb7785c0 --- /dev/null +++ b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt @@ -0,0 +1,19 @@ +package app.aaps.spike.cmp + +import androidx.compose.runtime.Composable +import app.aaps.core.keys.interfaces.TextRef + +/** + * The seam the whole `:core:ui` question turns on. + * + * 54 files in `:core:ui` call `androidx.compose.ui.res.stringResource`, which does not exist off + * Android, and `PlusMinusEdit` reaches it indirectly through the same-package `stringResource(TextRef)` + * resolver. So "can this file move to commonMain" is really "can the TextRef resolver be an + * expect/actual". This is that shape, proved rather than assumed. + * + * Android keeps AAPT exactly as the app does today. iOS has no resource system here yet - and does + * not need one for the spike, because the question is whether the SHAPE compiles for Kotlin/Native, + * not whether iOS can render Czech. + */ +@Composable +expect fun stringResource(ref: TextRef): String diff --git a/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt b/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt new file mode 100644 index 000000000000..3908c74e5654 --- /dev/null +++ b/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt @@ -0,0 +1,22 @@ +package app.aaps.spike.cmp + +import androidx.compose.runtime.Composable +import app.aaps.core.keys.interfaces.TextRef + +/** + * iOS has no resource system wired up here, and does not need one to answer the spike's question. + * + * [TextRef.Named] is the form a real iOS client would resolve, through a generated name-to-text table + * built from the same `strings.xml` - the client only needs the system language, so that table is a + * small generated file rather than a resource framework. Until it exists, showing the name is honest: + * it is visibly wrong on screen rather than silently blank. + * + * [TextRef.AndroidRes] cannot be resolved off Android by construction. It is an `Int` from AAPT, so + * a module that still hands one out has not finished moving - which is the point of the type. + */ +@Composable +actual fun stringResource(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.Named -> ref.name + is TextRef.AndroidRes -> "?res:${ref.id}" +} From f400188f377c05ee4da04b1034f196f30b20ff20 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 09:13:41 +0200 Subject: [PATCH 032/146] :core:ui TextRef.Named --- .../src/main/kotlin/GenerateKeyStringsTask.kt | 53 +++++++++++++------ .../interfaces/resources/ResourceHelper.kt | 19 ++++++- core/keys/build.gradle.kts | 1 + .../app/aaps/core/keys/interfaces/TextRef.kt | 35 +++++++++--- core/ui/build.gradle.kts | 27 ++++++++++ .../aaps/core/ui/compose/AapsSearchField.kt | 6 +-- .../app/aaps/core/ui/compose/CarbTimeRow.kt | 12 ++--- .../aaps/core/ui/compose/ConfigPluginCard.kt | 6 +-- .../aaps/core/ui/compose/DateTimeSection.kt | 6 +-- .../app/aaps/core/ui/compose/EventTimeRow.kt | 8 +-- .../app/aaps/core/ui/compose/FormatUtils.kt | 9 ++-- .../ui/compose/ImportSummaryComponents.kt | 3 +- .../aaps/core/ui/compose/InsulinSelector.kt | 3 +- .../core/ui/compose/MasterOfflineBanner.kt | 6 +-- .../aaps/core/ui/compose/NumberInputRow.kt | 5 +- .../app/aaps/core/ui/compose/PlusMinusEdit.kt | 1 + .../aaps/core/ui/compose/ProtectionHost.kt | 3 +- .../aaps/core/ui/compose/SliderWithButtons.kt | 1 + .../aaps/core/ui/compose/TextRefResource.kt | 28 +++++++++- .../aaps/core/ui/compose/TimeRangePicker.kt | 6 +-- .../ui/compose/dialogs/DatePickerModal.kt | 7 +-- .../dialogs/ElementConfirmationDialog.kt | 4 +- .../core/ui/compose/dialogs/ErrorDialog.kt | 7 +-- .../ui/compose/dialogs/GlobalSnackbarHost.kt | 5 +- .../core/ui/compose/dialogs/OkCancelDialog.kt | 11 ++-- .../aaps/core/ui/compose/dialogs/OkDialog.kt | 7 +-- .../compose/dialogs/QueryAnyPasswordDialog.kt | 9 ++-- .../ui/compose/dialogs/QueryPasswordDialog.kt | 8 +-- .../ui/compose/dialogs/SetPasswordDialog.kt | 8 +-- .../ui/compose/dialogs/ThreeButtonDialog.kt | 5 +- .../ui/compose/dialogs/TimePickerModal.kt | 7 +-- .../ui/compose/dialogs/UnifiedAuthDialog.kt | 10 ++-- .../ui/compose/dialogs/ValueInputDialog.kt | 7 +-- .../ui/compose/dialogs/YesNoCancelDialog.kt | 15 +++--- .../compose/insulin/ConcentrationDropDown.kt | 6 ++- .../core/ui/compose/insulin/SelectInsulin.kt | 9 ++-- .../ui/compose/pickers/WeekDaySelector.kt | 4 +- .../preference/AdaptiveDoublePreference.kt | 5 +- .../preference/AdaptiveIntPreference.kt | 5 +- .../preference/AdaptiveIntentPreference.kt | 3 +- .../preference/AdaptiveListPreference.kt | 1 + .../AdaptiveMasterPasswordPreference.kt | 18 +++---- .../preference/AdaptivePasswordPreference.kt | 7 +-- .../preference/AdaptivePreferenceItem.kt | 3 +- .../preference/AdaptiveStringPreference.kt | 1 + .../preference/AdaptiveSwitchPreference.kt | 5 +- .../AdaptiveUnitDoublePreference.kt | 1 + .../ClickablePreferenceCategoryHeader.kt | 3 +- .../preference/InlinePreferenceItems.kt | 1 + .../ui/compose/preference/ListPreference.kt | 4 +- .../preference/PluginPreferencesScreen.kt | 4 +- .../preference/PreferenceSliderWithButtons.kt | 3 +- .../core/ui/compose/preference/SyncBadge.kt | 7 +-- .../compose/preference/TextFieldPreference.kt | 4 +- .../core/ui/compose/pump/BlePreCheckHost.kt | 15 +++--- .../aaps/core/ui/compose/pump/BleScanStep.kt | 8 +-- .../ui/compose/pump/ProfileGateWizardStep.kt | 11 ++-- .../ui/compose/pump/PumpActivityDialog.kt | 15 +++--- .../core/ui/compose/pump/PumpHistoryScreen.kt | 7 +-- .../siteRotation/ArrowSelectionDialog.kt | 5 +- .../ui/compose/siteRotation/SiteEntryList.kt | 5 +- .../siteRotation/SiteLocationPicker.kt | 21 ++++---- .../siteRotation/SiteLocationPickerScreen.kt | 9 ++-- .../siteRotation/SiteLocationSummary.kt | 11 ++-- .../siteRotation/SiteLocationWizardStep.kt | 7 +-- 65 files changed, 366 insertions(+), 190 deletions(-) diff --git a/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt b/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt index 46e794bebd8f..19b9662cffe3 100644 --- a/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt +++ b/buildSrc/src/main/kotlin/GenerateKeyStringsTask.kt @@ -45,6 +45,16 @@ abstract class GenerateKeyStringsTask : DefaultTask() { @get:Input abstract val packageName: Property + /** + * Which module owns these names, for example `keys` or `ui`. + * + * A string name is only unique inside one module - `ns_wifi_ssids` exists in both `:core:keys` + * and `:core:ui` with different translations - so the resolver needs to know which map to look + * in rather than guessing by order. + */ + @get:Input + abstract val owner: Property + /** Name of the platform neutral object, for example `KeysStrings`. */ @get:Input abstract val objectName: Property @@ -68,15 +78,16 @@ abstract class GenerateKeyStringsTask : DefaultTask() { @TaskAction fun generate() { val res = resDir.get().asFile - val baseFile = File(res, "values/strings.xml") - if (!baseFile.isFile) throw GradleException("No values/strings.xml under $res") + val baseDir = File(res, "values") + if (!baseDir.isDirectory) throw GradleException("No values/ directory under $res") - val names = readStringNames(baseFile) - if (names.isEmpty()) throw GradleException("No elements in $baseFile") + val names = readStringNames(baseDir) + if (names.isEmpty()) throw GradleException("No elements under $baseDir") val duplicates = names.groupBy { it }.filterValues { it.size > 1 }.keys if (duplicates.isNotEmpty()) { - throw GradleException("Duplicate string names in $baseFile: ${duplicates.sorted()}") + // AAPT would reject these too, so this cannot fire on a project that builds. + throw GradleException("Duplicate string names under $baseDir: ${duplicates.sorted()}") } val unsafe = names.filterNot { it.isSafeKotlinIdentifier() } @@ -86,7 +97,7 @@ abstract class GenerateKeyStringsTask : DefaultTask() { // so it has to be a deliberate act rather than something the generator papers over. throw GradleException( "These string names cannot be generated as Kotlin properties: ${unsafe.sorted()}. " + - "Rename them in $baseFile." + "Rename them under $baseDir." ) } @@ -114,7 +125,8 @@ abstract class GenerateKeyStringsTask : DefaultTask() { append(" * declaring code stays free of Android resource ids.\n") append(" */\n") append("object $obj {\n\n") - names.forEach { append(" val $it: TextRef = TextRef.Named(\"$it\")\n") } + val ownerName = owner.get() + names.forEach { append(" val $it: TextRef = TextRef.Named(\"$ownerName\", \"$it\")\n") } append("}\n") } ) @@ -170,8 +182,7 @@ abstract class GenerateKeyStringsTask : DefaultTask() { var empty = 0 var partial = 0 locales.forEach { dir -> - val file = File(dir, "strings.xml") - val present = if (file.isFile) readStringNames(file).toSet() else emptySet() + val present = readStringNames(dir).toSet() val missing = expected - present val extra = present - expected val state = when { @@ -197,14 +208,26 @@ abstract class GenerateKeyStringsTask : DefaultTask() { } } - private fun readStringNames(file: File): List { - val doc = DocumentBuilderFactory.newInstance() + /** + * Every `` in EVERY xml file of a `values*` directory, because that is what AAPT does. + * + * Reading only `strings.xml` is the obvious shortcut and it is wrong: `:core:ui` keeps 1059 + * strings in `strings.xml`, 32 in `protection.xml` and 60 in `strings_scene_wizard.xml`, and a + * generator that missed the other two would leave 92 names unresolvable while looking correct. + * Files with no `` at all - colors, styles, layout aliases - simply contribute nothing. + */ + private fun readStringNames(dir: File): List { + val files = dir.listFiles { f: File -> f.isFile && f.extension.equals("xml", ignoreCase = true) } + ?.sortedBy { it.name } + .orEmpty() + val builder = DocumentBuilderFactory.newInstance() .apply { isNamespaceAware = false } .newDocumentBuilder() - .parse(file) - val nodes = doc.getElementsByTagName("string") - return (0 until nodes.length).mapNotNull { i -> - nodes.item(i).attributes?.getNamedItem("name")?.nodeValue + return files.flatMap { file -> + val nodes = builder.parse(file).getElementsByTagName("string") + (0 until nodes.length).mapNotNull { i -> + nodes.item(i).attributes?.getNamedItem("name")?.nodeValue + } } } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index a1736a800faf..bde290f9febc 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -39,7 +39,7 @@ interface ResourceHelper { else gs(ref.id, *ref.args.toTypedArray()) is TextRef.Named -> { - val id = KeysStringIds.idOf(ref.name) + val id = keysIdOf(ref) when { id == null -> ref.name ref.args.isEmpty() -> gs(id) @@ -52,7 +52,7 @@ interface ResourceHelper { fun gsNotLocalised(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text is TextRef.AndroidRes -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) - is TextRef.Named -> KeysStringIds.idOf(ref.name) + is TextRef.Named -> keysIdOf(ref) ?.let { gsNotLocalised(it, *ref.args.toTypedArray()) } ?: ref.name } @@ -70,3 +70,18 @@ interface ResourceHelper { fun dpToPx(dp: Float): Int fun shortTextMode(): Boolean } + +/** + * Resolves a [TextRef.Named] that this module can see. + * + * `:core:interfaces` depends on `:core:keys` and nothing else that owns strings, so only `keys` is + * resolvable here. A name owned by another module falls back to showing the raw name - visibly wrong + * rather than silently blank. + * + * That is not a gap in practice today: the `ui`-owned names are used from Composables, and + * `app.aaps.core.ui.compose.stringResource` sits in `:core:ui`, which can see both maps. If a + * non-Compose caller ever needs a `ui` name, this is the place that has to learn about it - probably + * as a registry rather than another branch. + */ +private fun keysIdOf(ref: TextRef.Named): Int? = + if (ref.owner == "keys") KeysStringIds.idOf(ref.name) else null diff --git a/core/keys/build.gradle.kts b/core/keys/build.gradle.kts index 670d787fa366..edd65741929b 100644 --- a/core/keys/build.gradle.kts +++ b/core/keys/build.gradle.kts @@ -13,6 +13,7 @@ plugins { val generateKeyStrings = tasks.register("generateKeyStrings") { resDir.set(layout.projectDirectory.dir("src/androidMain/res")) packageName.set("app.aaps.core.keys") + owner.set("keys") objectName.set("KeysStrings") idsObjectName.set("KeysStringIds") reportFile.set(layout.buildDirectory.file("reports/keyStrings/translations.txt")) diff --git a/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt index 8a2fd12a3c63..38e67cb37c0c 100644 --- a/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt +++ b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/TextRef.kt @@ -45,16 +45,23 @@ sealed interface TextRef { * A string named rather than numbered, so the declaring code holds nothing Android specific. * * [name] is the `name` attribute from `strings.xml`. Take these from the generated object for - * the owning module - `KeysStrings` for `:core:keys` - never by writing the string out by hand, - * because only the generated form is checked against the XML at build time. + * the owning module - `KeysStrings` for `:core:keys`, `UiStrings` for `:core:ui` - never by + * writing the string out by hand, because only the generated form is checked against the XML at + * build time. * - * Each platform resolves the name its own way. Android looks it up in the generated id map and - * then reads it through `Resources`, so locale matching and the always English lookup work - * exactly as they did when keys carried ids. + * [owner] says which module's `strings.xml` [name] came from, because a name is only unique + * within one module. `ns_wifi_ssids` really does exist in both `:core:keys` and `:core:ui` with + * different translations in Bulgarian, Norwegian and Chinese, so a resolver that guessed by + * lookup order would silently pick one of them. Tagging the owner makes it unambiguous and costs + * nothing at the call sites, because only the generator ever constructs this. + * + * Each platform resolves the pair its own way. Android looks it up in that module's generated id + * map and then reads it through `Resources`, so locale matching and the always English lookup + * work exactly as they did when keys carried ids. * * [args] behave as in [AndroidRes]. */ - data class Named(val name: String, val args: List = emptyList()) : TextRef + data class Named(val owner: String, val name: String, val args: List = emptyList()) : TextRef /** * Text that is only known at run time - a scanned pump name, a wiki page title, a user label. @@ -63,4 +70,20 @@ sealed interface TextRef { * no resource here", which callers had to remember to test for. */ data class Literal(val text: String) : TextRef + + companion object { + + /** + * The same reference with format arguments attached. + * + * The generated objects hand out argument-free references, because a generator cannot know + * what a call site wants to substitute. This is how a call site supplies them: + * `UiStrings.some_format.withArgs(count, unit)`. + */ + fun TextRef.withArgs(vararg args: Any): TextRef = when (this) { + is Named -> copy(args = args.toList()) + is AndroidRes -> copy(args = args.toList()) + is Literal -> this + } + } } diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 8222ddbdf6e0..771241a2ae2c 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min plugins { @@ -20,6 +21,32 @@ android { } } +// Same generator as :core:keys, pointed at this module's strings. It removes R.string from the +// Compose call sites so they stop being Android-only; the strings themselves do not move, and AAPT +// keeps resolving them on Android exactly as before. +extensions.configure("androidComponents") { + onVariants { variant -> + val taskProvider = tasks.register( + "generate${variant.name.replaceFirstChar { it.uppercase() }}UiStrings", + GenerateKeyStringsTask::class.java + ) { + resDir.set(layout.projectDirectory.dir("src/main/res")) + packageName.set("app.aaps.core.ui") + owner.set("ui") + objectName.set("UiStrings") + idsObjectName.set("UiStringIds") + reportFile.set(layout.buildDirectory.file("reports/uiStrings/${variant.name}-translations.txt")) + // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the + // task name, so both properties would land on one directory and the second file written + // would delete the first. + commonOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/${variant.name}/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/${variant.name}/android")) + } + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) + } +} + dependencies { api(libs.androidx.appcompat) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt index 6932bc92c430..4084d66abf71 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -17,7 +18,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -40,7 +40,7 @@ fun AapsSearchField( query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier, - placeholder: String = stringResource(R.string.search), + placeholder: String = stringResource(UiStrings.search), ) { val focusManager = LocalFocusManager.current @@ -67,7 +67,7 @@ fun AapsSearchField( IconButton(onClick = { onQueryChange("") }) { Icon( Icons.Filled.Clear, - contentDescription = stringResource(R.string.clear), + contentDescription = stringResource(UiStrings.clear), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt index d7aacfe1b9ba..3dd3f883b460 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -26,7 +27,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R @@ -82,13 +82,13 @@ fun CarbTimeRow( modifier = Modifier.weight(1f) ) { Text( - text = stringResource(R.string.wizard_carb_time) + ": ", + text = stringResource(UiStrings.wizard_carb_time) + ": ", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) if (offsetMinutes == 0) { Text( - text = stringResource(R.string.carb_time_now), + text = stringResource(UiStrings.carb_time_now), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface ) @@ -120,7 +120,7 @@ fun CarbTimeRow( if (!expanded) { FilledTonalButton(onClick = { expanded = true }) { - Text(stringResource(R.string.change)) + Text(stringResource(UiStrings.change)) } } } @@ -167,7 +167,7 @@ fun CarbTimeRow( modifier = Modifier.size(20.dp) ) Text( - text = stringResource(R.string.wizard_set_alarm), + text = stringResource(UiStrings.wizard_set_alarm), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface.copy(alpha = alarmAlpha) ) @@ -192,7 +192,7 @@ fun CarbTimeRow( @Composable private fun formatOffset(minutes: Int): String { return when { - minutes == 0 -> stringResource(R.string.carb_time_now) + minutes == 0 -> stringResource(UiStrings.carb_time_now) minutes > 0 -> "+${formatMinutesAsDuration(minutes)}" else -> "-${formatMinutesAsDuration(-minutes)}" } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt index dfb8f11ea100..69acb1fdf73c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -32,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -148,7 +148,7 @@ fun ConfigPluginCard( if (showSettings) { InCardActionRow( leadingIcon = Icons.Filled.Settings, - label = stringResource(R.string.settings), + label = stringResource(UiStrings.settings), onClick = onSettingsClick ) } @@ -158,7 +158,7 @@ fun ConfigPluginCard( } InCardActionRow( leadingIcon = Icons.AutoMirrored.Filled.OpenInNew, - label = stringResource(R.string.open_plugin), + label = stringResource(UiStrings.open_plugin), onClick = onOpenPluginClick ) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt index 45f5cdc17c69..2760b3fc6dc0 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -14,7 +15,6 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -47,7 +47,7 @@ fun DateTimeSection( onValueChange = {}, readOnly = true, enabled = false, - label = { Text(stringResource(R.string.date)) }, + label = { Text(stringResource(UiStrings.date)) }, trailingIcon = { Icon( imageVector = Icons.Filled.DateRange, @@ -72,7 +72,7 @@ fun DateTimeSection( onValueChange = {}, readOnly = true, enabled = false, - label = { Text(stringResource(R.string.time)) }, + label = { Text(stringResource(UiStrings.time)) }, trailingIcon = { Icon( imageVector = Icons.Outlined.Schedule, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt index cd325f150cf1..79b3a872e24b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -18,7 +19,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -58,12 +58,12 @@ fun EventTimeRow( modifier = Modifier.weight(1f) ) { Text( - text = stringResource(R.string.time) + ": ", + text = stringResource(UiStrings.time) + ": ", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = if (timeChanged) displayText else stringResource(R.string.carb_time_now), + text = if (timeChanged) displayText else stringResource(UiStrings.carb_time_now), style = MaterialTheme.typography.titleMedium, color = if (timeChanged) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface @@ -72,7 +72,7 @@ fun EventTimeRow( if (!expanded) { FilledTonalButton(onClick = { expanded = true }) { - Text(stringResource(R.string.change)) + Text(stringResource(UiStrings.change)) } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt index 4e4177ad0448..671e9e05cabe 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt @@ -1,7 +1,8 @@ package app.aaps.core.ui.compose -import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings +import androidx.compose.runtime.Composable import app.aaps.core.data.format.NumberFormat import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.TextRef @@ -20,10 +21,10 @@ fun formatMinutesAsDuration(minutes: Int): String { return if (abs >= 60) { val hours = abs / 60 val mins = abs % 60 - sign + if (mins == 0) stringResource(R.string.format_hours_only, hours) - else stringResource(R.string.format_hour_minute, hours, mins) + sign + if (mins == 0) stringResource(UiStrings.format_hours_only, hours) + else stringResource(UiStrings.format_hour_minute, hours, mins) } else { - stringResource(R.string.format_mins, minutes) + stringResource(UiStrings.format_mins, minutes) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt index e7eb4dff8a81..356fddf8bd22 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.maintenance.PrefMetadata import app.aaps.core.interfaces.maintenance.PrefsMetadataKey diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt index 529a9e75fbff..4e1756812f06 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -15,7 +17,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.data.model.ICfg import app.aaps.core.ui.R diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt index 200611c99555..3512fd1c366d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -14,7 +15,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -34,8 +34,8 @@ import app.aaps.core.ui.R fun MasterOfflineBanner( editingEnabled: Boolean, modifier: Modifier = Modifier, - text: String = if (!LocalMasterControlAllowed.current) stringResource(R.string.master_control_disabled_banner) - else stringResource(R.string.master_offline_banner) + text: String = if (!LocalMasterControlAllowed.current) stringResource(UiStrings.master_control_disabled_banner) + else stringResource(UiStrings.master_offline_banner) ) { if (editingEnabled) return Surface( diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index 5570923f5322..701844f42a3f 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -132,7 +133,7 @@ fun NumberInputRow( val rangeText = "${effectiveValueFormat.format(valueRange.start)} — ${effectiveValueFormat.format(valueRange.endInclusive)}" // Pre-resolve error strings for use in non-composable validateAndCommit - val errorInvalidNumber = stringResource(R.string.invalid_number) + val errorInvalidNumber = stringResource(UiStrings.invalid_number) fun validateAndCommit(text: String) { val cleaned = text.trim().replace(",", ".") diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt index b5d8898917fb..9e05f0710793 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt index 8ef0cc5ccb52..88c9dd4b3d0b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -7,7 +9,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.interfaces.protection.AuthorizationResult diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt index b590628aeb0f..16690a84d152 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import androidx.compose.ui.res.stringResource import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt index 9fdfe59a94c9..4c1799b589d0 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs +import app.aaps.core.ui.UiStringIds /** * Resolves a [TextRef] to text inside a Composable. @@ -24,7 +26,7 @@ fun stringResource(ref: TextRef): String = when (ref) { else stringResource(ref.id, *ref.args.toTypedArray()) is TextRef.Named -> { - val id = KeysStringIds.idOf(ref.name) + val id = androidIdOf(ref) when { id == null -> ref.name ref.args.isEmpty() -> stringResource(id) @@ -33,6 +35,30 @@ fun stringResource(ref: TextRef): String = when (ref) { } } +/** + * Which module's generated id map to look in. + * + * A name is only unique within one module, so this dispatches on the owner rather than trying the + * maps in some order - `ns_wifi_ssids` exists in both with different translations, and guessing + * would silently pick one. + * + * `:core:ui` can see both maps because it depends on `:core:keys`. A module that converts later adds + * its own branch here, or this becomes a registry once there are enough of them to be worth one. + */ +private fun androidIdOf(ref: TextRef.Named): Int? = when (ref.owner) { + "keys" -> KeysStringIds.idOf(ref.name) + "ui" -> UiStringIds.idOf(ref.name) + else -> null +} + +/** + * Same, with format arguments - mirrors `androidx.compose.ui.res.stringResource(id, vararg)`, which + * is what the call sites used before they stopped naming resource ids. + */ +@Composable +fun stringResource(ref: TextRef, vararg formatArgs: Any): String = + stringResource(ref.withArgs(*formatArgs)) + /** Same, for an optional reference - returns null so callers can keep using `?.let { }`. */ @Composable fun stringResourceOrNull(ref: TextRef?): String? = ref?.let { stringResource(it) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt index 0c9718f270a4..2940fe94ec62 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R import app.aaps.core.ui.compose.dialogs.TimePickerModal @@ -75,7 +75,7 @@ fun TimeRangePicker( modifier = Modifier.clickable { showStartPicker = true } ) { Text( - text = stringResource(R.string.from_label), + text = stringResource(UiStrings.from_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -100,7 +100,7 @@ fun TimeRangePicker( modifier = Modifier.clickable { showEndPicker = true } ) { Text( - text = stringResource(R.string.to_label), + text = stringResource(UiStrings.to_label), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt index 94ebbe772eaa..b3472cc09c5d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api @@ -7,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.R /** @@ -35,12 +36,12 @@ fun DatePickerModal( onDateSelected(datePickerState.selectedDateMillis) onDismiss() }) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } } ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt index e6ea694c32d2..ac9c5b694f1d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt @@ -1,7 +1,9 @@ package app.aaps.core.ui.compose.dialogs -import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings +import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt index 462bbb74c7d3..317ef0230067 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning @@ -10,7 +12,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign @@ -68,7 +69,7 @@ fun ErrorDialog( }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.dismiss)) + Text(stringResource(UiStrings.dismiss)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) @@ -118,7 +119,7 @@ fun ErrorDialog( }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.dismiss)) + Text(stringResource(UiStrings.dismiss)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt index 1c172a513333..108b6b553d33 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.navigationBarsPadding @@ -25,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner @@ -87,7 +88,7 @@ fun GlobalSnackbarHost( dismissAction = { TextButton(onClick = { hostState.currentSnackbarData?.dismiss() }) { Text( - text = stringResource(R.string.dismiss), + text = stringResource(UiStrings.dismiss), color = contentColor ) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt index bf6129c844a7..d474c8a41fcb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -15,7 +17,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign @@ -91,12 +92,12 @@ fun OkCancelDialog( }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false) @@ -161,12 +162,12 @@ fun OkCancelDialog( }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt index c7bda532aca0..9c6259fd6553 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon @@ -8,7 +10,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign @@ -47,7 +48,7 @@ fun OkDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) @@ -79,7 +80,7 @@ fun OkDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt index e5ced5088e39..067ec42c98a7 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -25,7 +27,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -105,7 +106,7 @@ fun QueryAnyPasswordDialog( OutlinedTextField( value = passwordText, onValueChange = { passwordText = it }, - label = { Text(stringResource(R.string.protection_password_hint)) }, + label = { Text(stringResource(UiStrings.protection_password_hint)) }, isError = errorMessage != null, supportingText = errorMessage?.let { msg -> { Text(msg, color = MaterialTheme.colorScheme.error) } }, visualTransformation = PasswordVisualTransformation(), @@ -136,12 +137,12 @@ fun QueryAnyPasswordDialog( onConfirm(passwordText) } ) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt index 97a283f4e607..5669368688dd 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.dialogs +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -20,7 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -102,12 +104,12 @@ fun QueryPasswordDialog( onConfirm(passwordText) } ) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt index f536952172a6..c86854b49236 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.dialogs +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -21,7 +24,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -114,12 +116,12 @@ fun SetPasswordDialog( }, confirmButton = { TextButton(onClick = { onConfirm(password1, password2) }) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt index c10478902545..47232d67aa39 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -18,7 +20,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign @@ -66,7 +67,7 @@ fun ThreeButtonDialog( cancelLabel: String? = null, onDismiss: () -> Unit ) { - val resolvedCancel = cancelLabel ?: stringResource(R.string.cancel) + val resolvedCancel = cancelLabel ?: stringResource(UiStrings.cancel) AlertDialog( onDismissRequest = onDismiss, icon = icon?.let { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt index 48814c20928e..fbe44dab2b25 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text @@ -7,7 +9,6 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties import app.aaps.core.ui.R @@ -45,12 +46,12 @@ fun TimePickerModal( onTimeSelected(timePickerState.hour, timePickerState.minute) onDismiss() }) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt index 2c225ccf5899..d615da680f88 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.dialogs +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -20,7 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -95,7 +97,7 @@ fun UnifiedAuthDialog( }, title = { Text( - text = stringResource(R.string.biometric_title), + text = stringResource(UiStrings.biometric_title), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) @@ -134,12 +136,12 @@ fun UnifiedAuthDialog( tryAuthenticate(passwordText) } ) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = { onResult(AuthorizationResult(null, ProtectionResult.CANCELLED)) }) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } }, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt index b772f22b20e9..8460f6b2c5d6 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,7 +21,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -185,12 +186,12 @@ fun ValueInputDialog( }, confirmButton = { TextButton(onClick = { confirm() }) { - Text(stringResource(R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } } ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt index c7e1949b3a41..3861a0ead6b2 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog @@ -7,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign @@ -47,16 +48,16 @@ fun YesNoCancelDialog( }, confirmButton = { TextButton(onClick = onYes) { - Text(stringResource(R.string.yes)) + Text(stringResource(UiStrings.yes)) } }, dismissButton = { Row { TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } TextButton(onClick = onNo) { - Text(stringResource(R.string.no)) + Text(stringResource(UiStrings.no)) } } }, @@ -89,16 +90,16 @@ fun YesNoCancelDialog( }, confirmButton = { TextButton(onClick = onYes) { - Text(stringResource(R.string.yes)) + Text(stringResource(UiStrings.yes)) } }, dismissButton = { Row { TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) + Text(stringResource(UiStrings.cancel)) } TextButton(onClick = onNo) { - Text(stringResource(R.string.no)) + Text(stringResource(UiStrings.no)) } } }, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt index d58800b1e629..0fed855ab0b9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.insulin +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -14,7 +17,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.insulin.ConcentrationType import app.aaps.core.ui.R @@ -37,7 +39,7 @@ fun ConcentrationDropdown( onValueChange = {}, readOnly = true, enabled = enabled, - label = { Text(stringResource(R.string.concentration_label)) }, + label = { Text(stringResource(UiStrings.concentration_label)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier .fillMaxWidth() diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt index 9e3f0b3dddc2..ddd2faf8a51c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.insulin +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -20,7 +22,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.aaps.core.data.model.ICfg @@ -77,7 +78,7 @@ fun SelectInsulin( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = stringResource(R.string.current_insulin), + text = stringResource(UiStrings.current_insulin), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -88,7 +89,7 @@ fun SelectInsulin( ) } FilledTonalButton(onClick = { expanded = !expanded }) { - Text(stringResource(R.string.change_insulin)) + Text(stringResource(UiStrings.change_insulin)) } } @@ -138,7 +139,7 @@ fun SelectInsulin( ) if (isActive) { Text( - text = stringResource(R.string.current_insulin), + text = stringResource(UiStrings.current_insulin), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt index ac24cb50c322..01726cb7042e 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.pickers +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -8,7 +11,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.elements.WeekDay diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index 485ae71f4a0d..7e9b457635aa 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -4,13 +4,14 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.decimalPlaces import app.aaps.core.keys.interfaces.DoublePreferenceKey @@ -120,7 +121,7 @@ fun AdaptiveDoublePreferenceItem( val summaryText = if (rangeRef != null) { stringResource(rangeRef) } else { - stringResource(R.string.preference_range_summary, valueFormat.format(value), unitLabelText, valueFormat.format(doubleKey.min), valueFormat.format(doubleKey.max)) + stringResource(UiStrings.preference_range_summary, valueFormat.format(value), unitLabelText, valueFormat.format(doubleKey.min), valueFormat.format(doubleKey.max)) } TextFieldPreference( state = state, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index ab7306c0eb79..f4a11b9cfec5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -4,13 +4,14 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.TextRef @@ -113,7 +114,7 @@ fun AdaptiveIntPreferenceItem( val summaryText = if (rangeRef != null) { stringResource(rangeRef) } else { - stringResource(R.string.preference_range_summary, value.toString(), unitLabelText, intKey.min.toString(), intKey.max.toString()) + stringResource(UiStrings.preference_range_summary, value.toString(), unitLabelText, intKey.min.toString(), intKey.max.toString()) } TextFieldPreference( state = state, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index 0c31c9228f50..db23bcd0b5ef 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -4,6 +4,8 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -11,7 +13,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.IntentPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt index 599836dd8a92..6e21b24832a3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt @@ -4,6 +4,7 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt index 6036c4b40d07..9d5cb3e23d2c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt @@ -1,12 +1,12 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource import app.aaps.core.keys.StringKey import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.R @@ -48,9 +48,9 @@ fun AdaptiveMasterPasswordPreferenceItem( val hasPassword = passwordState.isNotEmpty() val summary = if (hasPassword) { - stringResource(R.string.password_set) + stringResource(UiStrings.password_set) } else { - stringResource(R.string.password_not_set) + stringResource(UiStrings.password_not_set) } // Dialog states @@ -81,16 +81,16 @@ fun AdaptiveMasterPasswordPreferenceItem( ) // Message strings (resolved here for use in callbacks) - val wrongPasswordMsg = stringResource(R.string.wrongpassword) - val dontMatchMsg = stringResource(R.string.passwords_dont_match) - val passwordSetMsg = stringResource(R.string.password_set) - val passwordClearedMsg = stringResource(R.string.password_cleared) - val notChangedMsg = stringResource(R.string.password_not_changed) + val wrongPasswordMsg = stringResource(UiStrings.wrongpassword) + val dontMatchMsg = stringResource(UiStrings.passwords_dont_match) + val passwordSetMsg = stringResource(UiStrings.password_set) + val passwordClearedMsg = stringResource(UiStrings.password_cleared) + val notChangedMsg = stringResource(UiStrings.password_not_changed) // Query current password dialog if (showQueryDialog) { QueryPasswordDialog( - title = stringResource(R.string.current_master_password), + title = stringResource(UiStrings.current_master_password), pinInput = false, onConfirm = { enteredPassword -> if (checkPassword(enteredPassword, passwordState)) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt index 218f8f6cd595..8d02d1827d08 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt @@ -4,13 +4,14 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.TextRef @@ -70,8 +71,8 @@ fun AdaptivePasswordPreferenceItem( val summary = when { hasValue -> "••••••••" - isPin -> stringResource(R.string.pin_not_set) - else -> stringResource(R.string.password_not_set) + isPin -> stringResource(UiStrings.pin_not_set) + else -> stringResource(UiStrings.password_not_set) } Preference( diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index 038c27f06d5e..bf2ddabbe456 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -5,10 +5,11 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.res.stringResource import app.aaps.core.keys.PreferenceType import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.BooleanPreferenceKey diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index 068b189c4450..947fa94541d1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -4,6 +4,7 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.OutlinedTextField diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt index b1307c27cf1a..b7bb834fe5db 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt @@ -4,13 +4,14 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource import app.aaps.core.keys.interfaces.BooleanKeyWithChangeGuard import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.TextRef @@ -92,7 +93,7 @@ fun AdaptiveSwitchPreferenceItem( // Show guard rejection dialog guardMessage?.let { message -> OkDialog( - title = stringResource(R.string.error), + title = stringResource(UiStrings.error), message = message, onDismiss = { guardMessage = null } ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index 3b93b2e38b5c..71495ec95e18 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -4,6 +4,7 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt index a12e07223780..808a60fe6130 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt @@ -17,6 +17,8 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -40,7 +42,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.aaps.core.keys.interfaces.TextRef diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt index e4fb839efc8a..fe1fcf8e1255 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt index 56754be887a7..fe5337b374d9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt @@ -17,6 +17,9 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -45,7 +48,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt index 4d475c040c3d..9c633307e74c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -23,7 +26,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.ui.compose.AapsTopAppBar diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt index 2e3702660742..73f0b085c9b6 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import app.aaps.core.keys.interfaces.TextRef import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -15,7 +17,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt index da3e6617c5d1..2212009c3dff 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent @@ -13,7 +15,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.TextStyle @@ -42,7 +43,7 @@ fun SyncBadge(visible: Boolean, modifier: Modifier = Modifier) { // NOT a tappable refresh/sync control. Icon( imageVector = Icons.Default.PhonelinkRing, - contentDescription = stringResource(R.string.pref_syncs_with_main_phone), + contentDescription = stringResource(UiStrings.pref_syncs_with_main_phone), modifier = modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) @@ -84,7 +85,7 @@ fun TextWithSyncBadge( ) { Icon( imageVector = Icons.Default.PhonelinkRing, - contentDescription = stringResource(R.string.pref_syncs_with_main_phone), + contentDescription = stringResource(UiStrings.pref_syncs_with_main_phone), modifier = Modifier.fillMaxSize(), tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt index 841a074fac77..98a99b26b070 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt @@ -17,6 +17,9 @@ package app.aaps.core.ui.compose.preference +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -37,7 +40,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt index 22efefa9786b..a70aacbfa0ec 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.pump +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -7,7 +9,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.pump.BlePreCheck import app.aaps.core.interfaces.pump.BlePreCheckResult import app.aaps.core.ui.R @@ -47,8 +48,8 @@ fun BlePreCheckHost( when (checkResult) { BlePreCheckResult.BLE_NOT_SUPPORTED -> { OkDialog( - title = stringResource(R.string.message), - message = stringResource(R.string.ble_not_supported), + title = stringResource(UiStrings.message), + message = stringResource(UiStrings.ble_not_supported), onDismiss = { checkResult = null onFailed?.invoke() @@ -58,8 +59,8 @@ fun BlePreCheckHost( BlePreCheckResult.BLE_NOT_ENABLED -> { OkDialog( - title = stringResource(R.string.message), - message = stringResource(R.string.ble_not_enabled), + title = stringResource(UiStrings.message), + message = stringResource(UiStrings.ble_not_enabled), onDismiss = { checkResult = null onFailed?.invoke() @@ -69,8 +70,8 @@ fun BlePreCheckHost( BlePreCheckResult.PERMISSIONS_MISSING -> { OkDialog( - title = stringResource(R.string.message), - message = stringResource(R.string.ble_permissions_missing), + title = stringResource(UiStrings.message), + message = stringResource(UiStrings.ble_permissions_missing), onDismiss = { checkResult = null onFailed?.invoke() diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt index e6ed985f7023..757237989b9f 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt @@ -1,5 +1,8 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.pump.ble.ScannedDevice import app.aaps.core.ui.R @@ -46,8 +48,8 @@ fun BleScanStep( onStopScan: () -> Unit = {}, onCancel: () -> Unit, deviceNameFilter: Regex? = null, - title: String = stringResource(R.string.ble_scan_select_pump), - subtitle: String = stringResource(R.string.ble_scan_scanning) + title: String = stringResource(UiStrings.ble_scan_select_pump), + subtitle: String = stringResource(UiStrings.ble_scan_scanning) ) { DisposableEffect(Unit) { onStartScan() diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt index a2940fad4fbd..4bcf508ec23b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.pump +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -13,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.ui.R @@ -52,18 +53,18 @@ fun ProfileGateWizardStep(host: ProfileGateStepHost) { WizardStepLayout( primaryButton = if (hasStore) WizardButton( - text = stringResource(R.string.activate_profile), + text = stringResource(UiStrings.activate_profile), onClick = { host.activateSelectedProfile() }, enabled = selected != null ) else null, secondaryButton = WizardButton( - text = stringResource(R.string.cancel), + text = stringResource(UiStrings.cancel), onClick = { host.cancelGate() } ) ) { if (hasStore) { Text( - text = stringResource(R.string.pump_wizard_profile_gate_pick), + text = stringResource(UiStrings.pump_wizard_profile_gate_pick), style = MaterialTheme.typography.bodyLarge ) Column( @@ -95,7 +96,7 @@ fun ProfileGateWizardStep(host: ProfileGateStepHost) { } } else { Text( - text = stringResource(R.string.pump_wizard_profile_gate_no_store), + text = stringResource(UiStrings.pump_wizard_profile_gate_no_store), style = MaterialTheme.typography.bodyLarge ) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt index fa176f28c43f..8fae5723153a 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.pump +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -163,7 +164,7 @@ private fun BolusProgressSection( ) { // Title Text( - text = stringResource(R.string.goingtodeliver, state.insulin), + text = stringResource(UiStrings.goingtodeliver, state.insulin), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, @@ -214,7 +215,7 @@ private fun BolusProgressSection( // only hides this dialog — it does NOT stop the pump. state.stalled -> { Text( - text = stringResource(R.string.clientcontrol_bolus_progress_stalled_title), + text = stringResource(UiStrings.clientcontrol_bolus_progress_stalled_title), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error, @@ -223,7 +224,7 @@ private fun BolusProgressSection( ) Spacer(modifier = Modifier.height(AapsSpacing.medium)) Text( - text = stringResource(R.string.clientcontrol_bolus_progress_stalled_body), + text = stringResource(UiStrings.clientcontrol_bolus_progress_stalled_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, @@ -237,7 +238,7 @@ private fun BolusProgressSection( // Neutral/tonal — Dismiss only hides this local view; it is NOT destructive like Stop, // so it must not borrow Stop's error-red affordance. FilledTonalButton(onClick = onDismiss) { - Text(text = stringResource(R.string.dismiss)) + Text(text = stringResource(UiStrings.dismiss)) } } } @@ -257,8 +258,8 @@ private fun BolusProgressSection( ) ) { Text( - text = if (state.stopPressed) stringResource(R.string.stop_pressed) - else stringResource(R.string.stop) + text = if (state.stopPressed) stringResource(UiStrings.stop_pressed) + else stringResource(UiStrings.stop) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt index 62ecd7247a77..4df0b4fb3cd5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.pump +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.ui.R @@ -69,7 +70,7 @@ fun PumpHistoryScreen( if (state.isLoading) { CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) } else { - Text(stringResource(R.string.refresh)) + Text(stringResource(UiStrings.refresh)) } } } @@ -92,7 +93,7 @@ fun PumpHistoryScreen( contentAlignment = Alignment.Center ) { Text( - text = stringResource(R.string.no_history_records), + text = stringResource(UiStrings.no_history_records), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt index b6405bc7a8c1..b9244d0a6f5a 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,7 +13,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE import app.aaps.core.ui.R @@ -26,7 +27,7 @@ fun ArrowSelectionDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.select_arrow)) }, + title = { Text(stringResource(UiStrings.select_arrow)) }, text = { Column { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt index 897fe26b225f..a425d581bb27 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -25,7 +27,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE import app.aaps.core.interfaces.utils.DateUtil @@ -162,7 +163,7 @@ private fun SiteEntryRow( ) { Icon( imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit_site), + contentDescription = stringResource(UiStrings.edit_site), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.primary ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt index 7790c6ea16e4..fc9002e7ed5d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -31,7 +33,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE import app.aaps.core.ui.R @@ -100,9 +101,9 @@ fun SiteLocationPicker( ) { Text( text = if (selectedLocation != TE.Location.NONE) - stringResource(R.string.selected_location, selectedLocationString ?: selectedLocation.text) + stringResource(UiStrings.selected_location, selectedLocationString ?: selectedLocation.text) else - stringResource(R.string.select_location), + stringResource(UiStrings.select_location), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f) ) @@ -112,7 +113,7 @@ fun SiteLocationPicker( ) { Icon( imageVector = selectedArrow.directionToComposeIcon(), - contentDescription = stringResource(R.string.select_arrow), + contentDescription = stringResource(UiStrings.select_arrow), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurface ) @@ -135,7 +136,7 @@ fun SiteLocationPicker( ) { Icon( imageVector = IcCannulaChange, - contentDescription = stringResource(R.string.careportal_pump_site_management), + contentDescription = stringResource(UiStrings.careportal_pump_site_management), modifier = Modifier.size(24.dp) ) } @@ -148,7 +149,7 @@ fun SiteLocationPicker( ) { Icon( imageVector = IcCgmInsert, - contentDescription = stringResource(R.string.careportal_cgm_site_management), + contentDescription = stringResource(UiStrings.careportal_cgm_site_management), modifier = Modifier.size(24.dp) ) } @@ -160,7 +161,7 @@ fun SiteLocationPicker( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { - Text(stringResource(R.string.site_filter_info)) + Text(stringResource(UiStrings.site_filter_info)) } }, state = tooltipState @@ -279,7 +280,7 @@ fun SiteLocationPickerWithFilters( ) { Icon( imageVector = IcCannulaChange, - contentDescription = stringResource(R.string.careportal_pump_site_management), + contentDescription = stringResource(UiStrings.careportal_pump_site_management), modifier = Modifier.size(24.dp) ) } @@ -291,7 +292,7 @@ fun SiteLocationPickerWithFilters( ) { Icon( imageVector = IcCgmInsert, - contentDescription = stringResource(R.string.careportal_cgm_site_management), + contentDescription = stringResource(UiStrings.careportal_cgm_site_management), modifier = Modifier.size(24.dp) ) } @@ -303,7 +304,7 @@ fun SiteLocationPickerWithFilters( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { - Text(stringResource(R.string.site_filter_info)) + Text(stringResource(UiStrings.site_filter_info)) } }, state = tooltipState diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt index d5b7b4b4a05c..8db2b73bb7e4 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -16,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import app.aaps.core.data.model.TE import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsTopAppBar @@ -42,12 +43,12 @@ fun SiteLocationPickerScreen( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(R.string.site_rotation)) }, + title = { Text(stringResource(UiStrings.site_rotation)) }, navigationIcon = { IconButton(onClick = onClose) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(R.string.close) + contentDescription = stringResource(UiStrings.close) ) } }, @@ -58,7 +59,7 @@ fun SiteLocationPickerScreen( ) { Icon( imageVector = Icons.Default.Check, - contentDescription = stringResource(R.string.save), + contentDescription = stringResource(UiStrings.save), tint = if (selectedLocation != TE.Location.NONE) MaterialTheme.colorScheme.primary else diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt index ee51fd8b23eb..b1016d035b6f 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,7 +16,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE import app.aaps.core.ui.R @@ -65,21 +66,21 @@ fun SiteLocationSummary( if (hasSelection) { Text( - text = stringResource(R.string.selected_location, selectedLocationString), + text = stringResource(UiStrings.selected_location, selectedLocationString), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, modifier = Modifier.weight(1f) ) } else if (lastLocationString != null) { Text( - text = stringResource(R.string.last_site_location, lastLocationString), + text = stringResource(UiStrings.last_site_location, lastLocationString), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f) ) } else { Text( - text = stringResource(R.string.select_site_location), + text = stringResource(UiStrings.select_site_location), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f) @@ -87,7 +88,7 @@ fun SiteLocationSummary( } OutlinedButton(onClick = onPickSiteClick) { - Text(stringResource(R.string.pick_site)) + Text(stringResource(UiStrings.pick_site)) } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt index c1a3882637c0..254315a7290b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt @@ -1,8 +1,9 @@ package app.aaps.core.ui.compose.siteRotation +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.model.TE import app.aaps.core.ui.R @@ -40,12 +41,12 @@ fun SiteLocationWizardStep(host: SiteLocationStepHost) { WizardStepLayout( primaryButton = WizardButton( - text = stringResource(R.string.next), + text = stringResource(UiStrings.next), onClick = { host.completeSiteLocation() }, enabled = siteLocation != TE.Location.NONE ), secondaryButton = WizardButton( - text = stringResource(R.string.skip), + text = stringResource(UiStrings.skip), onClick = { host.updateSiteLocation(TE.Location.NONE) host.updateSiteArrow(TE.Arrow.NONE) From f2cd4b8a6b0d7178c323a7cdd494cc6ca7b876c6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 12:38:36 +0200 Subject: [PATCH 033/146] :core:ui element labels --- .../dialogs/ElementConfirmationDialog.kt | 9 +- .../ui/compose/navigation/ElementTypeStyle.kt | 204 +++++++++--------- .../app/aaps/core/ui/search/SearchableItem.kt | 8 +- .../navigation/ElementTypeStyleTest.kt | 6 +- .../CalibrationDialogScreen.kt | 5 +- .../compose/carbsDialog/CarbsDialogScreen.kt | 5 +- .../ExtendedBolusDialogScreen.kt | 5 +- .../ui/compose/fillDialog/FillDialogScreen.kt | 5 +- .../insulinDialog/InsulinDialogScreen.kt | 5 +- .../app/aaps/ui/compose/main/MainDrawer.kt | 11 +- .../compose/manageSheet/ManageBottomSheet.kt | 11 +- .../profileHelper/ProfileHelperScreen.kt | 5 +- .../ProfileManagementScreen.kt | 5 +- .../quickLaunch/QuickLaunchResolver.kt | 12 +- .../QuickWizardManagementScreen.kt | 5 +- .../aaps/ui/compose/scenes/SceneListScreen.kt | 5 +- .../tempBasalDialog/TempBasalDialogScreen.kt | 5 +- .../tempTarget/TempTargetManagementScreen.kt | 5 +- .../treatmentDialog/TreatmentDialogScreen.kt | 5 +- .../treatmentsSheet/TreatmentBottomSheet.kt | 14 +- .../wizardDialog/WizardDialogScreen.kt | 5 +- 21 files changed, 180 insertions(+), 160 deletions(-) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt index ac9c5b694f1d..7d10cd318dd0 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.dialogs +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings @@ -9,14 +10,14 @@ import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.ui.compose.navigation.color import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label /** * Shared confirmation dialog for the action dialogs (wizard, treatment, insulin, temp-basal, care, …). * * Carries the [elementType]'s visual identity — its label as the title and its themed icon/tint — so each * dialog only supplies the per-action [message] (a colored [AnnotatedString] summary) and the confirm/dismiss - * callbacks, instead of re-inlining `OkCancelDialog(title = …labelResId(), icon = …icon(), iconTint = …color())` + * callbacks, instead of re-inlining `OkCancelDialog(title = …label(), icon = …icon(), iconTint = …color())` * in every screen. */ @Composable @@ -27,7 +28,7 @@ fun ElementConfirmationDialog( onDismiss: () -> Unit ) { OkCancelDialog( - title = stringResource(elementType.labelResId()), + title = (stringResourceOrNull(elementType.label()) ?: ""), message = message, icon = elementType.icon(), iconTint = elementType.color(), @@ -61,7 +62,7 @@ fun ElementConfirmationDialog( onDismiss: () -> Unit ) { OkCancelDialog( - title = stringResource(elementType.labelResId()), + title = (stringResourceOrNull(elementType.label()) ?: ""), message = message, icon = elementType.icon(), iconTint = elementType.color(), diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt index 350207b23551..4a4308f4cf89 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose.navigation +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.UiStrings import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ExitToApp import androidx.compose.material.icons.filled.Add @@ -185,113 +187,113 @@ fun ElementType.icon(): ImageVector = when (this) { ElementType.EXIT -> Icons.AutoMirrored.Filled.ExitToApp } -fun ElementCategory.labelResId(): Int = when (this) { - ElementCategory.TREATMENT -> R.string.overview_treatment_label - ElementCategory.CGM -> R.string.cgm - ElementCategory.MANAGEMENT -> R.string.manage - ElementCategory.CAREPORTAL -> R.string.careportal - ElementCategory.DEVICE -> R.string.device_maintenance - ElementCategory.BASAL -> R.string.basal +fun ElementCategory.label(): TextRef? = when (this) { + ElementCategory.TREATMENT -> UiStrings.overview_treatment_label + ElementCategory.CGM -> UiStrings.cgm + ElementCategory.MANAGEMENT -> UiStrings.manage + ElementCategory.CAREPORTAL -> UiStrings.careportal + ElementCategory.DEVICE -> UiStrings.device_maintenance + ElementCategory.BASAL -> UiStrings.basal ElementCategory.SYSTEM, ElementCategory.NAVIGATION, - ElementCategory.INTERNAL -> 0 + ElementCategory.INTERNAL -> null } -fun ElementType.labelResId(): Int = when (this) { - ElementType.INSULIN -> R.string.overview_insulin_label - ElementType.CARBS -> R.string.carbs - ElementType.BOLUS_WIZARD -> R.string.boluswizard - ElementType.QUICK_WIZARD -> 0 // dynamic label - ElementType.QUICK_WIZARD_MANAGEMENT -> R.string.quickwizard_managemnt - ElementType.FOOD_MANAGEMENT -> R.string.food_management - ElementType.TREATMENT -> R.string.overview_treatment_label - ElementType.CGM_XDRIP -> R.string.cgm - ElementType.CGM_DEX -> R.string.cgm - ElementType.CALIBRATION -> R.string.calibration - ElementType.INSULIN_MANAGEMENT -> R.string.insulin_management - ElementType.PROFILE_MANAGEMENT -> R.string.profile_management - ElementType.TEMP_TARGET_MANAGEMENT -> R.string.temp_target_management - ElementType.BG_CHECK -> R.string.careportal_bgcheck - ElementType.NOTE -> R.string.careportal_note - ElementType.EXERCISE -> R.string.careportal_exercise - ElementType.QUESTION -> R.string.careportal_question - ElementType.ANNOUNCEMENT -> R.string.careportal_announcement - ElementType.SENSOR_INSERT -> R.string.cgm_sensor_insert - ElementType.BATTERY_CHANGE -> R.string.pump_battery_change - ElementType.CANNULA_CHANGE -> R.string.careportal_pump_site_change - ElementType.FILL -> R.string.prime_fill - ElementType.SITE_ROTATION -> R.string.site_rotation - ElementType.TEMP_BASAL -> R.string.temp_basal - ElementType.EXTENDED_BOLUS -> R.string.extended_bolus - ElementType.AUTOMATION -> 0 // dynamic label - ElementType.AUTOMATION_MANAGEMENT -> R.string.automation - ElementType.PUMP -> R.string.pump - ElementType.SETTINGS -> R.string.settings - ElementType.QUICK_LAUNCH_CONFIG -> R.string.quick_launch_configure - ElementType.TREATMENTS -> R.string.treatments_history - ElementType.STATISTICS -> R.string.statistics - ElementType.TDD_CYCLE_PATTERN -> R.string.tdd_cycle_pattern - ElementType.PROFILE_HELPER -> R.string.nav_profile_helper - ElementType.HISTORY_BROWSER -> R.string.nav_history_browser - ElementType.SETUP_WIZARD -> R.string.nav_setupwizard - ElementType.MAINTENANCE -> R.string.maintenance - ElementType.CONFIGURATION -> R.string.nav_configuration - ElementType.ABOUT -> R.string.nav_about - ElementType.COB -> R.string.cob - ElementType.SENSITIVITY -> R.string.sensitivity - ElementType.SCENE -> 0 // dynamic label - ElementType.SCENE_MANAGEMENT -> R.string.scene_management - ElementType.AUTHORIZED_CLIENTS -> R.string.authorized_clients_manage_label - ElementType.PAIR_WITH_MASTER -> R.string.pair_with_master_manage_label - ElementType.RUNNING_MODE -> R.string.running_mode - ElementType.USER_ENTRY -> R.string.user_entry - ElementType.LOOP -> R.string.loop - ElementType.AAPS -> R.string.aaps - ElementType.EXIT -> R.string.nav_exit +fun ElementType.label(): TextRef? = when (this) { + ElementType.INSULIN -> UiStrings.overview_insulin_label + ElementType.CARBS -> UiStrings.carbs + ElementType.BOLUS_WIZARD -> UiStrings.boluswizard + ElementType.QUICK_WIZARD -> null // dynamic label + ElementType.QUICK_WIZARD_MANAGEMENT -> UiStrings.quickwizard_managemnt + ElementType.FOOD_MANAGEMENT -> UiStrings.food_management + ElementType.TREATMENT -> UiStrings.overview_treatment_label + ElementType.CGM_XDRIP -> UiStrings.cgm + ElementType.CGM_DEX -> UiStrings.cgm + ElementType.CALIBRATION -> UiStrings.calibration + ElementType.INSULIN_MANAGEMENT -> UiStrings.insulin_management + ElementType.PROFILE_MANAGEMENT -> UiStrings.profile_management + ElementType.TEMP_TARGET_MANAGEMENT -> UiStrings.temp_target_management + ElementType.BG_CHECK -> UiStrings.careportal_bgcheck + ElementType.NOTE -> UiStrings.careportal_note + ElementType.EXERCISE -> UiStrings.careportal_exercise + ElementType.QUESTION -> UiStrings.careportal_question + ElementType.ANNOUNCEMENT -> UiStrings.careportal_announcement + ElementType.SENSOR_INSERT -> UiStrings.cgm_sensor_insert + ElementType.BATTERY_CHANGE -> UiStrings.pump_battery_change + ElementType.CANNULA_CHANGE -> UiStrings.careportal_pump_site_change + ElementType.FILL -> UiStrings.prime_fill + ElementType.SITE_ROTATION -> UiStrings.site_rotation + ElementType.TEMP_BASAL -> UiStrings.temp_basal + ElementType.EXTENDED_BOLUS -> UiStrings.extended_bolus + ElementType.AUTOMATION -> null // dynamic label + ElementType.AUTOMATION_MANAGEMENT -> UiStrings.automation + ElementType.PUMP -> UiStrings.pump + ElementType.SETTINGS -> UiStrings.settings + ElementType.QUICK_LAUNCH_CONFIG -> UiStrings.quick_launch_configure + ElementType.TREATMENTS -> UiStrings.treatments_history + ElementType.STATISTICS -> UiStrings.statistics + ElementType.TDD_CYCLE_PATTERN -> UiStrings.tdd_cycle_pattern + ElementType.PROFILE_HELPER -> UiStrings.nav_profile_helper + ElementType.HISTORY_BROWSER -> UiStrings.nav_history_browser + ElementType.SETUP_WIZARD -> UiStrings.nav_setupwizard + ElementType.MAINTENANCE -> UiStrings.maintenance + ElementType.CONFIGURATION -> UiStrings.nav_configuration + ElementType.ABOUT -> UiStrings.nav_about + ElementType.COB -> UiStrings.cob + ElementType.SENSITIVITY -> UiStrings.sensitivity + ElementType.SCENE -> null // dynamic label + ElementType.SCENE_MANAGEMENT -> UiStrings.scene_management + ElementType.AUTHORIZED_CLIENTS -> UiStrings.authorized_clients_manage_label + ElementType.PAIR_WITH_MASTER -> UiStrings.pair_with_master_manage_label + ElementType.RUNNING_MODE -> UiStrings.running_mode + ElementType.USER_ENTRY -> UiStrings.user_entry + ElementType.LOOP -> UiStrings.loop + ElementType.AAPS -> UiStrings.aaps + ElementType.EXIT -> UiStrings.nav_exit } -fun ElementType.descriptionResId(): Int = when (this) { - ElementType.INSULIN -> R.string.treatment_insulin_desc - ElementType.CARBS -> R.string.treatment_carbs_desc - ElementType.BOLUS_WIZARD -> R.string.treatment_calculator_desc - ElementType.TREATMENT -> R.string.treatment_desc - ElementType.INSULIN_MANAGEMENT -> R.string.manage_insulin_desc - ElementType.PROFILE_MANAGEMENT -> R.string.manage_profile_desc - ElementType.TEMP_TARGET_MANAGEMENT -> R.string.manage_temp_target_desc - ElementType.QUICK_WIZARD_MANAGEMENT -> R.string.manage_quickwizard_desc - ElementType.FOOD_MANAGEMENT -> R.string.manage_food_desc - - ElementType.TEMP_BASAL -> R.string.manage_temp_basal_desc - ElementType.EXTENDED_BOLUS -> R.string.manage_extended_bolus_desc - ElementType.SITE_ROTATION -> R.string.manage_site_rotation_desc - ElementType.NOTE -> R.string.treatment_note_desc - ElementType.QUESTION -> R.string.treatment_question_desc +fun ElementType.description(): TextRef? = when (this) { + ElementType.INSULIN -> UiStrings.treatment_insulin_desc + ElementType.CARBS -> UiStrings.treatment_carbs_desc + ElementType.BOLUS_WIZARD -> UiStrings.treatment_calculator_desc + ElementType.TREATMENT -> UiStrings.treatment_desc + ElementType.INSULIN_MANAGEMENT -> UiStrings.manage_insulin_desc + ElementType.PROFILE_MANAGEMENT -> UiStrings.manage_profile_desc + ElementType.TEMP_TARGET_MANAGEMENT -> UiStrings.manage_temp_target_desc + ElementType.QUICK_WIZARD_MANAGEMENT -> UiStrings.manage_quickwizard_desc + ElementType.FOOD_MANAGEMENT -> UiStrings.manage_food_desc + + ElementType.TEMP_BASAL -> UiStrings.manage_temp_basal_desc + ElementType.EXTENDED_BOLUS -> UiStrings.manage_extended_bolus_desc + ElementType.SITE_ROTATION -> UiStrings.manage_site_rotation_desc + ElementType.NOTE -> UiStrings.treatment_note_desc + ElementType.QUESTION -> UiStrings.treatment_question_desc ElementType.CGM_XDRIP, - ElementType.CGM_DEX -> R.string.treatment_cgm_desc - - ElementType.CALIBRATION -> R.string.treatment_calibration_desc - ElementType.BG_CHECK -> R.string.treatment_bg_check_desc - ElementType.EXERCISE -> R.string.treatment_exercise_desc - ElementType.ANNOUNCEMENT -> R.string.treatment_announcement_desc - ElementType.SENSOR_INSERT -> R.string.treatment_sensor_insert_desc - ElementType.BATTERY_CHANGE -> R.string.treatment_battery_change_desc - ElementType.CANNULA_CHANGE -> R.string.treatment_cannula_change_desc - ElementType.FILL -> R.string.treatment_fill_desc - ElementType.TREATMENTS -> R.string.treatments_desc - ElementType.STATISTICS -> R.string.statistics_desc - ElementType.TDD_CYCLE_PATTERN -> R.string.tdd_cycle_pattern_desc - ElementType.PROFILE_HELPER -> R.string.nav_profile_helper_desc - ElementType.HISTORY_BROWSER -> R.string.nav_history_browser_desc - ElementType.SETUP_WIZARD -> R.string.nav_setupwizard_desc - ElementType.MAINTENANCE -> R.string.description_maintenance - ElementType.CONFIGURATION -> R.string.nav_configuration_desc - ElementType.ABOUT -> R.string.nav_about_desc - ElementType.QUICK_LAUNCH_CONFIG -> R.string.quick_launch_configure_desc - ElementType.SCENE -> R.string.scene_desc - ElementType.SCENE_MANAGEMENT -> R.string.scene_management_desc - ElementType.AUTOMATION_MANAGEMENT -> R.string.automation_management_desc - ElementType.AUTHORIZED_CLIENTS -> R.string.authorized_clients_manage_desc - ElementType.PAIR_WITH_MASTER -> R.string.pair_with_master_manage_desc + ElementType.CGM_DEX -> UiStrings.treatment_cgm_desc + + ElementType.CALIBRATION -> UiStrings.treatment_calibration_desc + ElementType.BG_CHECK -> UiStrings.treatment_bg_check_desc + ElementType.EXERCISE -> UiStrings.treatment_exercise_desc + ElementType.ANNOUNCEMENT -> UiStrings.treatment_announcement_desc + ElementType.SENSOR_INSERT -> UiStrings.treatment_sensor_insert_desc + ElementType.BATTERY_CHANGE -> UiStrings.treatment_battery_change_desc + ElementType.CANNULA_CHANGE -> UiStrings.treatment_cannula_change_desc + ElementType.FILL -> UiStrings.treatment_fill_desc + ElementType.TREATMENTS -> UiStrings.treatments_desc + ElementType.STATISTICS -> UiStrings.statistics_desc + ElementType.TDD_CYCLE_PATTERN -> UiStrings.tdd_cycle_pattern_desc + ElementType.PROFILE_HELPER -> UiStrings.nav_profile_helper_desc + ElementType.HISTORY_BROWSER -> UiStrings.nav_history_browser_desc + ElementType.SETUP_WIZARD -> UiStrings.nav_setupwizard_desc + ElementType.MAINTENANCE -> UiStrings.description_maintenance + ElementType.CONFIGURATION -> UiStrings.nav_configuration_desc + ElementType.ABOUT -> UiStrings.nav_about_desc + ElementType.QUICK_LAUNCH_CONFIG -> UiStrings.quick_launch_configure_desc + ElementType.SCENE -> UiStrings.scene_desc + ElementType.SCENE_MANAGEMENT -> UiStrings.scene_management_desc + ElementType.AUTOMATION_MANAGEMENT -> UiStrings.automation_management_desc + ElementType.AUTHORIZED_CLIENTS -> UiStrings.authorized_clients_manage_desc + ElementType.PAIR_WITH_MASTER -> UiStrings.pair_with_master_manage_desc ElementType.QUICK_WIZARD, ElementType.RUNNING_MODE, ElementType.AUTOMATION, @@ -302,5 +304,5 @@ fun ElementType.descriptionResId(): Int = when (this) { ElementType.USER_ENTRY, ElementType.LOOP, ElementType.AAPS, - ElementType.EXIT -> 0 + ElementType.EXIT -> null } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt index 117c5bb4c505..7c912690b7ef 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt @@ -5,9 +5,9 @@ import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.keys.interfaces.PreferenceKey import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.compose.navigation.descriptionResId +import app.aaps.core.ui.compose.navigation.description import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef /** @@ -93,11 +93,11 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = elementType.name - override val title: TextRef = TextRef.AndroidRes(elementType.labelResId()) + override val title: TextRef = elementType.label() ?: TextRef.Literal("") @Deprecated("use icon") override val icon: ImageVector = elementType.icon() - override val summary: TextRef? = elementType.descriptionResId().takeIf { it != 0 }?.let { TextRef.AndroidRes(it) } + override val summary: TextRef? = elementType.description() } /** diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt index 08184e5b9a84..76fc846e5a69 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt @@ -51,13 +51,13 @@ class ElementTypeStyleTest { @Test fun typesWithZeroLabel_matchDocumentedDynamicSet() { - val actualZero = ElementType.entries.filter { it.labelResId() == 0 }.toSet() + val actualZero = ElementType.entries.filter { it.label() == null }.toSet() assertThat(actualZero).isEqualTo(typesWithDynamicLabel) } @Test fun typesWithZeroDescription_matchDocumentedSet() { - val actualZero = ElementType.entries.filter { it.descriptionResId() == 0 }.toSet() + val actualZero = ElementType.entries.filter { it.description() == null }.toSet() assertThat(actualZero).isEqualTo(typesWithoutDescription) } @@ -65,7 +65,7 @@ class ElementTypeStyleTest { fun searchableEntries_haveDisplayableLabel() { // A search hit with no label and no dynamic-label fallback would show as a blank row. val blank = ElementType.searchableEntries.filter { - it.labelResId() == 0 && it !in typesWithDynamicLabel + it.label() == null && it !in typesWithDynamicLabel } assertThat(blank).isEmpty() } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt index 8c8710462a1c..b4d4ee054468 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/calibrationDialog/CalibrationDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.calibrationDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -53,7 +54,7 @@ import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.R import app.aaps.core.ui.R as CoreUiR @@ -135,7 +136,7 @@ internal fun CalibrationDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.CALIBRATION.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.CALIBRATION.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt index 88518c562c9c..bbafe41dbd90 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.carbsDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -63,7 +64,7 @@ import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.consumeOverscroll import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.ui.compose.EventDatePicker @@ -230,7 +231,7 @@ internal fun CarbsDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.CARBS.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.CARBS.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt index 0826f48256b8..0703bd5f86e0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.extendedBolusDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -47,7 +48,7 @@ import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog import app.aaps.core.ui.compose.dialogs.OkCancelDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.R as CoreUiR @Composable @@ -131,7 +132,7 @@ internal fun ExtendedBolusDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.EXTENDED_BOLUS.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.EXTENDED_BOLUS.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt index 63f48db5490c..c35271dbd92e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.fillDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -64,7 +65,7 @@ import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.consumeOverscroll import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog import app.aaps.core.ui.compose.insulin.SelectInsulin -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.core.ui.compose.siteRotation.SiteLocationSummary @@ -237,7 +238,7 @@ internal fun FillDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.FILL.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.FILL.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt index ad0e0f72974d..a4d2d852b0bc 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.insulinDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -61,7 +62,7 @@ import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.consumeOverscroll import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.ui.compose.EventDatePicker @@ -225,7 +226,7 @@ internal fun InsulinDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.INSULIN.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.INSULIN.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainDrawer.kt b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainDrawer.kt index 5770f72ccc0f..268f556806a7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainDrawer.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainDrawer.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.main +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -26,9 +27,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.ui.compose.navigation.NavigationRequest -import app.aaps.core.ui.compose.navigation.descriptionResId +import app.aaps.core.ui.compose.navigation.description import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label @Composable fun MainDrawer( @@ -96,11 +97,11 @@ private fun DrawerMenuItem( enabled: Boolean = true, onClick: () -> Unit ) { - val descResId = elementType.descriptionResId() + val desc = elementType.description() DrawerMenuItem( icon = elementType.icon(), - label = stringResource(elementType.labelResId()), - description = if (descResId != 0) stringResource(descResId) else null, + label = (stringResourceOrNull(elementType.label()) ?: ""), + description = stringResourceOrNull(desc), enabled = enabled, onClick = onClick ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageBottomSheet.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageBottomSheet.kt index 84e04c8d383c..f443e9f4aa12 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageBottomSheet.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageBottomSheet.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.manageSheet +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -53,9 +54,9 @@ import app.aaps.core.ui.compose.icons.IcTbrCancel import app.aaps.core.ui.compose.masterEditingEnabled import app.aaps.core.ui.compose.navigation.NavigationRequest import app.aaps.core.ui.compose.navigation.color -import app.aaps.core.ui.compose.navigation.descriptionResId +import app.aaps.core.ui.compose.navigation.description import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.rememberBringIntoViewOnExpand import app.aaps.core.ui.R as CoreUiR @@ -542,9 +543,9 @@ private fun ManageGridItem( enabled: Boolean = true ) { val color = elementType.color() - val label = text ?: stringResource(elementType.labelResId()) - val descResId = elementType.descriptionResId() - val description = if (descResId != 0) stringResource(descResId) else null + val label = text ?: (stringResourceOrNull(elementType.label()) ?: "") + val desc = elementType.description() + val description = stringResourceOrNull(desc) // A relayed action (visible only to master/paired-client) is also DISABLED when the master is unreachable or // has remote control turned off — not just hidden when unpaired. Non-relayed entries (Pump, Pair-with-master) // use their own visibility lambda, so they stay enabled (the only thing you can still do is re-pair). diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt index 1cc1ea362e49..348c614607db 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileHelper/ProfileHelperScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.profileHelper +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -64,7 +65,7 @@ import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.clearFocusOnTap -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.R import app.aaps.ui.compose.profileManagement.viewmodels.ProfileHelperViewModel import app.aaps.ui.compose.stats.TddStatsCompose @@ -322,7 +323,7 @@ internal fun ProfileHelperContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.PROFILE_HELPER.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.PROFILE_HELPER.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon(Icons.Filled.Close, contentDescription = stringResource(app.aaps.core.ui.R.string.close)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileManagementScreen.kt index 959f504f045f..06eeb875183e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/ProfileManagementScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.profileManagement +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -59,7 +60,7 @@ import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.ScreenMode import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.interfaces.navigation.ElementType -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.R import app.aaps.ui.compose.components.CarouselReorderConfig import app.aaps.ui.compose.components.ContentContainer @@ -197,7 +198,7 @@ fun ProfileManagementScreen( ) } else { AapsTopAppBar( - title = { Text(stringResource(ElementType.PROFILE_MANAGEMENT.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.PROFILE_MANAGEMENT.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt index 3acb97f93f45..fd24ee887806 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt @@ -22,9 +22,9 @@ import app.aaps.core.ui.compose.icons.IcTtActivity import app.aaps.core.ui.compose.icons.IcTtEatingSoon import app.aaps.core.ui.compose.icons.IcTtHypo import app.aaps.core.ui.compose.icons.IcTtManual -import app.aaps.core.ui.compose.navigation.descriptionResId +import app.aaps.core.ui.compose.navigation.description import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.compose.navigation.ElementAvailability import app.aaps.ui.compose.scenes.SceneIcons import app.aaps.core.interfaces.scenes.SceneStore @@ -134,8 +134,8 @@ class QuickLaunchResolver @Inject constructor( is QuickLaunchAction.PluginAction -> findPlugin(action.className)?.let { rh.gs(it.pluginDescription.pluginName) } ?: "?" else -> { - val resId = action.elementType?.labelResId() ?: 0 - if (resId != 0) rh.gs(resId) else action.typeId + val label = action.elementType?.label() + label?.let { rh.gs(it) } ?: action.typeId } } @@ -181,8 +181,8 @@ class QuickLaunchResolver @Inject constructor( ?.pluginDescription?.description?.takeIf { it != -1 }?.let { rh.gs(it) } else -> { - val resId = action.elementType?.descriptionResId() ?: 0 - if (resId != 0) rh.gs(resId) else null + val desc = action.elementType?.description() + desc?.let { rh.gs(it) } } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardManagementScreen.kt index c2fdcdf5e271..5c97fbc50d93 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/QuickWizardManagementScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.quickWizard +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement @@ -59,7 +60,7 @@ import app.aaps.core.ui.compose.clearFocusOnTap import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.ui.compose.masterEditingEnabled import app.aaps.core.interfaces.navigation.ElementType -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.R import app.aaps.ui.compose.components.CarouselReorderConfig import app.aaps.ui.compose.components.ContentContainer @@ -215,7 +216,7 @@ fun QuickWizardManagementScreen( ) } else { AapsTopAppBar( - title = { Text(stringResource(ElementType.QUICK_WIZARD_MANAGEMENT.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.QUICK_WIZARD_MANAGEMENT.label()) ?: "")) }, navigationIcon = { IconButton(onClick = { if (!isPlayMode && viewModel.hasUnsavedChanges()) { diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListScreen.kt index d3c2009051da..f89f30ffefe1 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.scenes +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -49,7 +50,7 @@ import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.MasterOfflineBanner import app.aaps.core.ui.compose.dialogs.OkDialog import app.aaps.core.ui.compose.dialogs.ThreeButtonDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -114,7 +115,7 @@ fun SceneListScreen( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.SCENE_MANAGEMENT.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.SCENE_MANAGEMENT.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt index d0dabc094e98..5925685e626b 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.tempBasalDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -46,7 +47,7 @@ import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.R as CoreUiR @Composable @@ -123,7 +124,7 @@ internal fun TempBasalDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.TEMP_BASAL.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.TEMP_BASAL.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementScreen.kt index 944081252c2d..47ca0b7259b7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.tempTarget +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -66,7 +67,7 @@ import app.aaps.core.ui.compose.dialogs.DatePickerModal import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.ui.compose.dialogs.TimePickerModal import app.aaps.core.ui.compose.masterEditingEnabled -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.R import app.aaps.ui.compose.components.CarouselReorderConfig import app.aaps.ui.compose.components.ContentContainer @@ -240,7 +241,7 @@ fun TempTargetManagementScreen( ) } else { AapsTopAppBar( - title = { Text(stringResource(ElementType.TEMP_TARGET_MANAGEMENT.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.TEMP_TARGET_MANAGEMENT.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt index 5b6b142a6214..cefaae572815 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.treatmentDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -47,7 +48,7 @@ import app.aaps.core.ui.compose.NumberInputRow import app.aaps.core.ui.compose.banner.WarningBanner import app.aaps.core.ui.compose.bottomBarSafeArea import app.aaps.core.ui.compose.dialogs.ElementConfirmationDialog -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.ui.compose.components.DialogStatusBar import app.aaps.ui.compose.overview.chips.CobUiState import app.aaps.ui.compose.overview.chips.IobUiState @@ -149,7 +150,7 @@ internal fun TreatmentDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.TREATMENT.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.TREATMENT.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentBottomSheet.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentBottomSheet.kt index c27ea0091486..0aa15f3f40a6 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentBottomSheet.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentBottomSheet.kt @@ -1,5 +1,7 @@ package app.aaps.ui.compose.treatmentsSheet +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -40,9 +42,9 @@ import app.aaps.core.ui.compose.icons.IcCarbs import app.aaps.core.ui.compose.masterEditingEnabled import app.aaps.core.ui.compose.navigation.NavigationRequest import app.aaps.core.ui.compose.navigation.color -import app.aaps.core.ui.compose.navigation.descriptionResId +import app.aaps.core.ui.compose.navigation.description import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.ui.compose.main.QuickWizardItem @@ -321,7 +323,7 @@ private fun TreatmentItem( onClick: () -> Unit ) { val color = elementType.color() - val descResId = elementType.descriptionResId() + val desc = elementType.description() // A relayed action (visible only to master/paired-client) is also DISABLED when the master is unreachable or // has remote control off. Local items (CGM, Calibration) use the default ALWAYS visibility → stay enabled. val effectiveEnabled = enabled && @@ -329,15 +331,15 @@ private fun TreatmentItem( ListItem( headlineContent = { Text( - text = stringResource(elementType.labelResId()), + text = (stringResourceOrNull(elementType.label()) ?: ""), color = if (effectiveEnabled) color else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha) ) }, - supportingContent = if (descResId != 0) { + supportingContent = if (desc != null) { { Text( - text = stringResource(descResId), + text = stringResource(desc), color = if (effectiveEnabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha) ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt index f942b5b4413a..cfe9af06f1c2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.wizardDialog +import app.aaps.core.ui.compose.stringResourceOrNull import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -92,7 +93,7 @@ import app.aaps.core.ui.compose.icons.IcPizza import app.aaps.core.ui.compose.icons.IcTtManual import app.aaps.core.ui.compose.navigation.color import app.aaps.core.ui.compose.navigation.icon -import app.aaps.core.ui.compose.navigation.labelResId +import app.aaps.core.ui.compose.navigation.label import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.core.ui.compose.rememberBringIntoViewOnExpand @@ -241,7 +242,7 @@ internal fun WizardDialogContent( Scaffold( topBar = { AapsTopAppBar( - title = { Text(stringResource(ElementType.BOLUS_WIZARD.labelResId())) }, + title = { Text((stringResourceOrNull(ElementType.BOLUS_WIZARD.label()) ?: "")) }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( From b64c3e167feeed774f5d2ecfb6ad36145bab010f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 13:17:56 +0200 Subject: [PATCH 034/146] :core:ui resource ids --- .../interfaces/resources/ResourceHelper.kt | 4 + .../ui/clientcontrol/FailureReasonText.kt | 40 ++++----- .../app/aaps/core/ui/compose/CarbTimeRow.kt | 4 +- .../app/aaps/core/ui/compose/FormatUtils.kt | 10 +-- .../aaps/core/ui/compose/InsulinSelector.kt | 5 +- .../aaps/core/ui/compose/NumberInputRow.kt | 12 +-- .../core/ui/compose/NumberInputRowPreviews.kt | 17 ++-- .../core/ui/compose/PluginCategoryTitle.kt | 26 +++--- .../core/ui/compose/SelectableListToolbar.kt | 1 + .../aaps/core/ui/compose/SliderWithButtons.kt | 8 +- .../ui/compose/SliderWithButtonsPreviews.kt | 3 +- .../app/aaps/core/ui/compose/UnitTypeText.kt | 82 ++++++++++--------- .../ui/compose/dialogs/QueryPasswordDialog.kt | 2 +- .../ui/compose/dialogs/SetPasswordDialog.kt | 4 +- .../ui/compose/dialogs/ThreeButtonDialog.kt | 2 +- .../ui/compose/dialogs/UnifiedAuthDialog.kt | 6 +- .../preference/AdaptiveDoublePreference.kt | 6 +- .../preference/AdaptiveIntPreference.kt | 6 +- .../preference/AdaptivePasswordPreference.kt | 8 +- .../preference/AdaptiveStringPreference.kt | 5 +- .../ClickablePreferenceCategoryHeader.kt | 2 +- .../CollapsibleCardSectionContentPreviews.kt | 3 +- .../preference/PluginPreferencesScreen.kt | 6 +- .../preference/PreferenceSliderWithButtons.kt | 6 +- .../preference/PreferenceSubScreenDef.kt | 3 +- .../aaps/core/ui/compose/pump/BleScanStep.kt | 2 +- .../compose/pump/PumpOverviewStateBuilder.kt | 1 + .../app/aaps/core/ui/elements/WeekDay.kt | 21 ++--- .../utils/HardLimitsImplTest.kt | 2 +- .../compose/elements/MiscElements.kt | 1 + .../wear/wearintegration/DataHandlerMobile.kt | 4 +- .../DataHandlerMobileWearBolusTest.kt | 2 + .../carbsDialog/CarbsDialogViewModel.kt | 6 +- .../ClientControlPendingDialog.kt | 6 +- .../configuration/ConfigurationModels.kt | 3 +- .../configuration/ConfigurationScreen.kt | 1 + .../configuration/ConfigurationViewModel.kt | 4 +- .../configuration/PluginCategoryScreen.kt | 1 + .../ExtendedBolusDialogViewModel.kt | 6 +- .../compose/fillDialog/FillDialogViewModel.kt | 4 +- .../insulinDialog/InsulinDialogViewModel.kt | 6 +- .../InsulinManagementViewModel.kt | 6 +- .../app/aaps/ui/compose/main/MainViewModel.kt | 12 +-- .../ui/compose/manageSheet/ManageViewModel.kt | 6 +- .../viewmodels/ProfileManagementViewModel.kt | 6 +- .../quickLaunch/QuickLaunchConfigScreen.kt | 1 + .../quickLaunch/QuickLaunchConfigViewModel.kt | 7 +- .../RunningModeManagementViewModel.kt | 4 +- .../ui/compose/scenes/SceneListViewModel.kt | 4 +- .../ui/compose/scenes/wizard/DurationStep.kt | 3 +- .../TempBasalDialogViewModel.kt | 6 +- .../TempTargetManagementViewModel.kt | 6 +- .../TreatmentDialogViewModel.kt | 6 +- .../wizardDialog/WizardDialogViewModel.kt | 4 +- .../fillDialog/FillDialogViewModelTest.kt | 2 + 55 files changed, 223 insertions(+), 191 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index bde290f9febc..e4687cec3c49 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -14,6 +14,7 @@ import androidx.annotation.RawRes import androidx.annotation.StringRes import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs interface ResourceHelper { @@ -48,6 +49,9 @@ interface ResourceHelper { } } + /** Same, with format arguments - mirrors `gs(id, vararg)`. */ + fun gs(ref: TextRef, vararg args: Any): String = gs(ref.withArgs(*args)) + /** Same, but always in English - used to build the search index. */ fun gsNotLocalised(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt index 8eeb079972f3..c8772a590c98 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.clientcontrol +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.UiStrings import androidx.annotation.StringRes import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.ui.R @@ -9,23 +11,23 @@ import app.aaps.core.ui.R * (`ClientControlPendingDialog`) and the wear relay error path (`DataHandlerMobile`). Resolve the returned id on * the SHOWING device (`stringResource` / `ResourceHelper.gs`) so the text is in that device's locale. */ -@StringRes -fun FailureReason.failTextResId(): Int = when (this) { - FailureReason.NotPaired -> R.string.clientcontrol_fail_not_paired - FailureReason.NotReachable -> R.string.clientcontrol_fail_not_reachable - FailureReason.NoReply -> R.string.clientcontrol_fail_no_reply - FailureReason.Expired -> R.string.clientcontrol_fail_expired - FailureReason.Busy -> R.string.clientcontrol_fail_busy - FailureReason.SendFailed -> R.string.clientcontrol_fail_send_failed - FailureReason.NoActiveProfile -> R.string.clientcontrol_fail_no_active_profile - FailureReason.SceneNotFound -> R.string.clientcontrol_fail_scene_not_found - FailureReason.SceneDisabled -> R.string.clientcontrol_fail_scene_disabled - FailureReason.PartialFailure -> R.string.clientcontrol_fail_partial - FailureReason.ExecutionFailed -> R.string.clientcontrol_fail_execution - FailureReason.ControlDisabled -> R.string.clientcontrol_fail_control_disabled - FailureReason.NoAction -> R.string.no_action_selected - FailureReason.NoPendingBolus -> R.string.clientcontrol_fail_no_pending_bolus - FailureReason.BolusComputeFailed -> R.string.clientcontrol_fail_bolus_compute - FailureReason.Internal -> R.string.clientcontrol_fail_internal - FailureReason.Unknown -> R.string.clientcontrol_fail_unknown + +fun FailureReason.failText(): TextRef = when (this) { + FailureReason.NotPaired -> UiStrings.clientcontrol_fail_not_paired + FailureReason.NotReachable -> UiStrings.clientcontrol_fail_not_reachable + FailureReason.NoReply -> UiStrings.clientcontrol_fail_no_reply + FailureReason.Expired -> UiStrings.clientcontrol_fail_expired + FailureReason.Busy -> UiStrings.clientcontrol_fail_busy + FailureReason.SendFailed -> UiStrings.clientcontrol_fail_send_failed + FailureReason.NoActiveProfile -> UiStrings.clientcontrol_fail_no_active_profile + FailureReason.SceneNotFound -> UiStrings.clientcontrol_fail_scene_not_found + FailureReason.SceneDisabled -> UiStrings.clientcontrol_fail_scene_disabled + FailureReason.PartialFailure -> UiStrings.clientcontrol_fail_partial + FailureReason.ExecutionFailed -> UiStrings.clientcontrol_fail_execution + FailureReason.ControlDisabled -> UiStrings.clientcontrol_fail_control_disabled + FailureReason.NoAction -> UiStrings.no_action_selected + FailureReason.NoPendingBolus -> UiStrings.clientcontrol_fail_no_pending_bolus + FailureReason.BolusComputeFailed -> UiStrings.clientcontrol_fail_bolus_compute + FailureReason.Internal -> UiStrings.clientcontrol_fail_internal + FailureReason.Unknown -> UiStrings.clientcontrol_fail_unknown } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt index 3dd3f883b460..bf3a3ca979b8 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt @@ -136,12 +136,12 @@ fun CarbTimeRow( // Offset input NumberInputRow( - labelResId = R.string.time, + labelRef = UiStrings.time, value = offsetMinutes.toDouble(), onValueChange = { onOffsetChange(it.toInt()) }, valueRange = offsetRange.first.toDouble()..offsetRange.last.toDouble(), step = offsetStep.toDouble(), - unitLabel = TextRef.AndroidRes(R.string.units_min) + unitLabel = UiStrings.units_min ) // Alarm toggle (disabled when offset <= 0) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt index 671e9e05cabe..6c322d63c179 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt @@ -51,7 +51,7 @@ fun formatMinutesAsDuration(minutes: Int, rh: ResourceHelper): String { * * Priority order: * 1. [asDuration] → "X h Y min" or "X min" - * 2. valueFormatResId → stringResource with value (as Int if formatAsInt, else Double) + * 2. valueFormat → stringResource with value (as Int if formatAsInt, else Double) * 3. unitLabel set → "formatted_value unitLabel" * 4. Plain → valueFormat.format(value) * @@ -63,7 +63,7 @@ fun formatMinutesAsDuration(minutes: Int, rh: ResourceHelper): String { fun formatSliderDisplayValue( value: Double, unitLabel: TextRef? = null, - valueFormatResId: Int? = null, + valueFormatRef: TextRef? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat, asDuration: Boolean = false @@ -72,9 +72,9 @@ fun formatSliderDisplayValue( return when { asDuration -> formatMinutesAsDuration(value.roundToInt()) - valueFormatResId != null -> { - if (formatAsInt) stringResource(valueFormatResId, value.roundToInt()) - else stringResource(valueFormatResId, value) + valueFormatRef != null -> { + if (formatAsInt) stringResource(valueFormatRef, value.roundToInt()) + else stringResource(valueFormatRef, value) } resolvedUnitLabel.isNotEmpty() -> "${valueFormat.format(value)} $resolvedUnitLabel" diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt index 4e1756812f06..ea8b5470746f 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth @@ -37,7 +38,7 @@ fun InsulinSelector( selected: ICfg?, onSelect: (ICfg) -> Unit, modifier: Modifier = Modifier, - labelResId: Int = R.string.select_insulin + label: TextRef = UiStrings.select_insulin ) { var expanded by remember { mutableStateOf(false) } @@ -50,7 +51,7 @@ fun InsulinSelector( value = selected?.insulinLabel ?: "", onValueChange = {}, readOnly = true, - label = { Text(stringResource(labelResId)) }, + label = { Text(stringResource(label)) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), modifier = Modifier diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index 701844f42a3f..e5a676c190b6 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -56,7 +56,7 @@ import kotlin.math.roundToInt * @param step Step increment for +/- buttons * @param unitLabel Unit label shown after the value * @param asDuration Render the value as "Xh Ym" instead of a plain number - * @param valueFormatResId Resource ID for formatting value with unit (e.g., "%1$.1f U") + * @param valueFormat Resource ID for formatting value with unit (e.g., "%1$.1f U") * @param formatAsInt If true, value is formatted as Int for stringResource (use with %d format strings) * @param valueFormat Custom NumberFormat (overrides auto-created from decimalPlaces) * @param decimalPlaces Number of decimal places for value display (0 = integer, default). Ignored if valueFormat is set. @@ -79,7 +79,7 @@ fun NumberInputRow( modifier: Modifier = Modifier, unitLabel: TextRef? = null, asDuration: Boolean = false, - valueFormatResId: Int? = null, + valueFormatRef: TextRef? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat? = null, decimalPlaces: Int = 0, @@ -119,7 +119,7 @@ fun NumberInputRow( val formattedDisplay = formatSliderDisplayValue( value = value, unitLabel = unitLabel, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = formatAsInt, valueFormat = effectiveValueFormat, asDuration = asDuration @@ -297,7 +297,7 @@ fun NumberInputRow( } /** - * Convenience for the many call sites that name their own module's `R.string.x`. + * Convenience for the many call sites that name their own module's `UiStrings.x`. * The [TextRef] form above is the real one; this just wraps the id. */ @Composable @@ -310,7 +310,7 @@ fun NumberInputRow( modifier: Modifier = Modifier, unitLabel: TextRef? = null, asDuration: Boolean = false, - valueFormatResId: Int? = null, + valueFormatRef: TextRef? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat? = null, decimalPlaces: Int = 0, @@ -326,7 +326,7 @@ fun NumberInputRow( modifier = modifier, unitLabel = unitLabel, asDuration = asDuration, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = formatAsInt, valueFormat = valueFormat, decimalPlaces = decimalPlaces, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt index 175f5d935763..51d627015d28 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview @@ -10,7 +11,7 @@ import app.aaps.core.ui.R @Composable internal fun NumberInputRowBasicPreview() { MaterialTheme { - NumberInputRow(labelResId = R.string.carbs, value = 20.0, onValueChange = {}, valueRange = 0.0..100.0, step = 1.0) + NumberInputRow(labelRef = UiStrings.carbs, value = 20.0, onValueChange = {}, valueRange = 0.0..100.0, step = 1.0) } } @@ -19,7 +20,7 @@ internal fun NumberInputRowBasicPreview() { internal fun NumberInputRowWithUnitPreview() { MaterialTheme { NumberInputRow( - labelResId = R.string.insulin_label, + labelRef = UiStrings.insulin_label, value = 3.5, onValueChange = {}, valueRange = 0.0..10.0, @@ -35,12 +36,12 @@ internal fun NumberInputRowWithUnitPreview() { internal fun NumberInputRowMinutesPreview() { MaterialTheme { NumberInputRow( - labelResId = R.string.duration, + labelRef = UiStrings.duration, value = 130.0, onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabel = TextRef.AndroidRes(R.string.units_min) + unitLabel = UiStrings.units_min ) } } @@ -50,12 +51,12 @@ internal fun NumberInputRowMinutesPreview() { internal fun NumberInputRowPercentPreview() { MaterialTheme { NumberInputRow( - labelResId = R.string.duration, + labelRef = UiStrings.duration, value = 100.0, onValueChange = {}, valueRange = 10.0..200.0, step = 5.0, - unitLabel = TextRef.AndroidRes(R.string.units_percent) + unitLabel = UiStrings.units_percent ) } } @@ -65,12 +66,12 @@ internal fun NumberInputRowPercentPreview() { internal fun NumberInputRowMinutesDirectPreview() { MaterialTheme { NumberInputRow( - labelResId = R.string.duration, + labelRef = UiStrings.duration, value = 130.0, onValueChange = {}, valueRange = 0.0..300.0, step = 10.0, - unitLabel = TextRef.AndroidRes(R.string.units_min) + unitLabel = UiStrings.units_min ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt index 77bd136deaaa..6c65834a5667 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.UiStrings import androidx.annotation.StringRes import app.aaps.core.data.plugin.PluginType import app.aaps.core.ui.R @@ -9,16 +11,16 @@ import app.aaps.core.ui.R * screen and the Quick-launch config. Exhaustive `when` (no `else`) so adding a [PluginType] is a * compile-forced decision here. */ -@StringRes -fun pluginCategoryTitleRes(type: PluginType): Int = when (type) { - PluginType.BGSOURCE -> R.string.configbuilder_bgsource - PluginType.SMOOTHING -> R.string.configbuilder_smoothing - PluginType.CALIBRATION -> R.string.configbuilder_calibration - PluginType.PUMP -> R.string.configbuilder_pump - PluginType.SENSITIVITY -> R.string.configbuilder_sensitivity - PluginType.APS -> R.string.configbuilder_aps - PluginType.LOOP -> R.string.configbuilder_loop - PluginType.CONSTRAINTS -> R.string.constraints - PluginType.SYNC -> R.string.configbuilder_sync - PluginType.GENERAL -> R.string.configbuilder_general + +fun pluginCategoryTitle(type: PluginType): TextRef = when (type) { + PluginType.BGSOURCE -> UiStrings.configbuilder_bgsource + PluginType.SMOOTHING -> UiStrings.configbuilder_smoothing + PluginType.CALIBRATION -> UiStrings.configbuilder_calibration + PluginType.PUMP -> UiStrings.configbuilder_pump + PluginType.SENSITIVITY -> UiStrings.configbuilder_sensitivity + PluginType.APS -> UiStrings.configbuilder_aps + PluginType.LOOP -> UiStrings.configbuilder_loop + PluginType.CONSTRAINTS -> UiStrings.constraints + PluginType.SYNC -> UiStrings.configbuilder_sync + PluginType.GENERAL -> UiStrings.configbuilder_general } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt index 1e6c1eea2042..0944adb85286 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt index 16690a84d152..1bb87d092e96 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt @@ -50,7 +50,7 @@ import kotlin.math.roundToInt * @param step The step size for +/- buttons (default 0.1) * @param controlPoints Pairs of (position [0-1], value) to create a non linear slider, if null slider is linear * @param showValue Whether to show a clickable value label (default false) - * @param valueFormatResId Resource ID for formatting value with unit (e.g., "%1$.1f U" or "%1$d min") + * @param valueFormat Resource ID for formatting value with unit (e.g., "%1$.1f U" or "%1$d min") * @param formatAsInt If true, value is formatted as Int for stringResource (use with %d format strings) * @param valueFormat Format for the value (used for dialog and fallback) * @param unitLabel Unit label, shown after the value and as the dialog input suffix @@ -72,7 +72,7 @@ fun SliderWithButtons( step: Double = 0.1, controlPoints: List>? = null, showValue: Boolean = false, - valueFormatResId: Int? = null, + valueFormatRef: TextRef? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, unitLabel: TextRef? = null, @@ -163,7 +163,7 @@ fun SliderWithButtons( val displayText = if (showValue) formatSliderDisplayValue( value = value, unitLabel = unitLabel, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = formatAsInt, valueFormat = valueFormat, asDuration = asDuration @@ -237,7 +237,7 @@ fun SliderWithButtons( color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.End, modifier = Modifier - .widthIn(min = if (asDuration || valueFormatResId != null || resolvedUnitLabel.isNotEmpty()) 70.dp else 40.dp) + .widthIn(min = if (asDuration || valueFormat != null || resolvedUnitLabel.isNotEmpty()) 70.dp else 40.dp) .then(if (enabled) Modifier.clickable { showDialog = true } else Modifier) .padding(start = 4.dp) ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt index 27ba75dddea1..982207c85743 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -48,7 +49,7 @@ internal fun SliderWithButtonsIntPreview() { step = 5.0, showValue = true, valueFormat = NumberFormat.INTEGER, - unitLabel = TextRef.AndroidRes(R.string.units_min) + unitLabel = UiStrings.units_min ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt index 8aa9d065abd5..2335d199ccf1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt @@ -1,5 +1,7 @@ package app.aaps.core.ui.compose +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs +import app.aaps.core.ui.UiStrings import app.aaps.core.keys.UnitType import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R @@ -18,16 +20,16 @@ import app.aaps.core.ui.R */ fun UnitType.unitLabel(): TextRef? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> TextRef.AndroidRes(R.string.units_grams) - UnitType.MIN -> TextRef.AndroidRes(R.string.units_min) - UnitType.SEC -> TextRef.AndroidRes(R.string.units_sec) - UnitType.HOURS, UnitType.HOURS_DOUBLE -> TextRef.AndroidRes(R.string.units_hours) - UnitType.DAYS -> TextRef.AndroidRes(R.string.units_days) - UnitType.PERCENT -> TextRef.AndroidRes(R.string.units_percent) - UnitType.INSULIN, UnitType.INSULIN_INT -> TextRef.AndroidRes(R.string.units_insulin) - UnitType.INSULIN_RATE -> TextRef.AndroidRes(R.string.units_insulin_rate) + UnitType.GRAMS -> UiStrings.units_grams + UnitType.MIN -> UiStrings.units_min + UnitType.SEC -> UiStrings.units_sec + UnitType.HOURS, UnitType.HOURS_DOUBLE -> UiStrings.units_hours + UnitType.DAYS -> UiStrings.units_days + UnitType.PERCENT -> UiStrings.units_percent + UnitType.INSULIN, UnitType.INSULIN_INT -> UiStrings.units_insulin + UnitType.INSULIN_RATE -> UiStrings.units_insulin_rate UnitType.DOUBLE, UnitType.DOUBLE_2, UnitType.DOUBLE_3 -> null // generic doubles carry no unit - UnitType.MGDL -> TextRef.AndroidRes(R.string.units_mgdl) + UnitType.MGDL -> UiStrings.units_mgdl } /** @@ -37,7 +39,7 @@ fun UnitType.unitLabel(): TextRef? = when (this) { * with no arguments renders the raw `%1$d` template. Taking them makes that mistake impossible. */ fun UnitType.rangeText(value: Any, min: Any, max: Any): TextRef? = - rangeFormatResId()?.let { TextRef.AndroidRes(it, listOf(value, min, max)) } + rangeFormat()?.withArgs(value, min, max) /** * Format template for a single value, e.g. `%1$d min`. @@ -45,40 +47,40 @@ fun UnitType.rangeText(value: Any, min: Any, max: Any): TextRef? = * Still a raw resource id, unlike the two above, because the slider applies it to whatever value * the user drags to - the arguments are not known here. It never leaves `:core:ui`. */ -fun UnitType.valueFormatResId(): Int? = when (this) { +fun UnitType.valueFormat(): TextRef? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> R.string.units_format_grams - UnitType.MIN -> R.string.units_format_min - UnitType.SEC -> R.string.units_format_sec - UnitType.HOURS -> R.string.units_format_hours - UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double - UnitType.DAYS -> R.string.units_format_days - UnitType.PERCENT -> R.string.units_format_percent - UnitType.INSULIN -> R.string.units_format_insulin - UnitType.INSULIN_INT -> R.string.units_format_insulin_int - UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate - UnitType.DOUBLE -> R.string.units_format_double - UnitType.DOUBLE_2 -> R.string.units_format_double_2 - UnitType.DOUBLE_3 -> R.string.units_format_double_3 - UnitType.MGDL -> R.string.units_format_mgdl + UnitType.GRAMS -> UiStrings.units_format_grams + UnitType.MIN -> UiStrings.units_format_min + UnitType.SEC -> UiStrings.units_format_sec + UnitType.HOURS -> UiStrings.units_format_hours + UnitType.HOURS_DOUBLE -> UiStrings.units_format_hours_double + UnitType.DAYS -> UiStrings.units_format_days + UnitType.PERCENT -> UiStrings.units_format_percent + UnitType.INSULIN -> UiStrings.units_format_insulin + UnitType.INSULIN_INT -> UiStrings.units_format_insulin_int + UnitType.INSULIN_RATE -> UiStrings.units_format_insulin_rate + UnitType.DOUBLE -> UiStrings.units_format_double + UnitType.DOUBLE_2 -> UiStrings.units_format_double_2 + UnitType.DOUBLE_3 -> UiStrings.units_format_double_3 + UnitType.MGDL -> UiStrings.units_format_mgdl } -private fun UnitType.rangeFormatResId(): Int? = when (this) { +private fun UnitType.rangeFormat(): TextRef? = when (this) { UnitType.NONE -> null - UnitType.GRAMS -> R.string.units_format_grams_range - UnitType.MIN -> R.string.units_format_min_range - UnitType.SEC -> R.string.units_format_sec_range - UnitType.HOURS -> R.string.units_format_hours_range - UnitType.HOURS_DOUBLE -> R.string.units_format_hours_double_range - UnitType.DAYS -> R.string.units_format_days_range - UnitType.PERCENT -> R.string.units_format_percent_range - UnitType.INSULIN -> R.string.units_format_insulin_range - UnitType.INSULIN_INT -> R.string.units_format_insulin_int_range - UnitType.INSULIN_RATE -> R.string.units_format_insulin_rate_range - UnitType.DOUBLE -> R.string.units_format_double_range - UnitType.DOUBLE_2 -> R.string.units_format_double_2_range - UnitType.DOUBLE_3 -> R.string.units_format_double_3_range - UnitType.MGDL -> R.string.units_format_mgdl_range + UnitType.GRAMS -> UiStrings.units_format_grams_range + UnitType.MIN -> UiStrings.units_format_min_range + UnitType.SEC -> UiStrings.units_format_sec_range + UnitType.HOURS -> UiStrings.units_format_hours_range + UnitType.HOURS_DOUBLE -> UiStrings.units_format_hours_double_range + UnitType.DAYS -> UiStrings.units_format_days_range + UnitType.PERCENT -> UiStrings.units_format_percent_range + UnitType.INSULIN -> UiStrings.units_format_insulin_range + UnitType.INSULIN_INT -> UiStrings.units_format_insulin_int_range + UnitType.INSULIN_RATE -> UiStrings.units_format_insulin_rate_range + UnitType.DOUBLE -> UiStrings.units_format_double_range + UnitType.DOUBLE_2 -> UiStrings.units_format_double_2_range + UnitType.DOUBLE_3 -> UiStrings.units_format_double_3_range + UnitType.MGDL -> UiStrings.units_format_mgdl_range } /** True when this unit should be rendered as a duration ("1 h 30 min") rather than a plain number. */ diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt index 5669368688dd..eda527822146 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt @@ -75,7 +75,7 @@ fun QueryPasswordDialog( value = passwordText, onValueChange = { passwordText = it }, label = { - Text(stringResource(if (pinInput) R.string.protection_pin_hint else R.string.protection_password_hint)) + Text(stringResource(if (pinInput) UiStrings.protection_pin_hint else UiStrings.protection_password_hint)) }, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt index c86854b49236..7a25a210135c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt @@ -82,7 +82,7 @@ fun SetPasswordDialog( value = password1, onValueChange = { password1 = it }, label = { - Text(stringResource(if (pinInput) R.string.protection_pin_hint else R.string.protection_password_hint)) + Text(stringResource(if (pinInput) UiStrings.protection_pin_hint else UiStrings.protection_password_hint)) }, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( @@ -99,7 +99,7 @@ fun SetPasswordDialog( value = password2, onValueChange = { password2 = it }, label = { - Text(stringResource(if (pinInput) R.string.confirm_pin_hint else R.string.confirm_password_hint)) + Text(stringResource(if (pinInput) UiStrings.confirm_pin_hint else UiStrings.confirm_password_hint)) }, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt index 47232d67aa39..d9b3a70e2246 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt @@ -48,7 +48,7 @@ import app.aaps.core.ui.R * @param onPrimary Called when primary button is clicked * @param secondaryLabel Label for the alternative action button (e.g., "End") * @param onSecondary Called when secondary button is clicked - * @param cancelLabel Optional override for cancel label; defaults to R.string.cancel + * @param cancelLabel Optional override for cancel label; defaults to UiStrings.cancel * @param onDismiss Called when cancel is clicked or dialog is dismissed * * @see ThreeButtonDialogPreview diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt index d615da680f88..89db88ce6ca8 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt @@ -59,9 +59,9 @@ fun UnifiedAuthDialog( // Pick hint: master password is always accepted, plus any custom credentials val hintRes = when { - hasPin -> R.string.auth_hint_master_or_pin - hasCustomPassword -> R.string.auth_hint_master_or_password - else -> R.string.auth_hint_master_password + hasPin -> UiStrings.auth_hint_master_or_pin + hasCustomPassword -> UiStrings.auth_hint_master_or_password + else -> UiStrings.auth_hint_master_password } var passwordText by remember { mutableStateOf("") } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index 7e9b457635aa..c93f8a8ff9ea 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -25,7 +25,7 @@ import app.aaps.core.ui.compose.rangeText import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.stringResourceOrNull import app.aaps.core.ui.compose.unitLabel -import app.aaps.core.ui.compose.valueFormatResId +import app.aaps.core.ui.compose.valueFormat /** * Composable double preference for use inside card sections. @@ -60,7 +60,7 @@ fun AdaptiveDoublePreferenceItem( val unitType = doubleKey.unitType val decimalPlaces = unitType.decimalPlaces() val step = unitType.step() - val valueFormatResId = unitType.valueFormatResId() + val valueFormatRef = unitType.valueFormat() // Get unit label from UnitType (for dialog input suffix) val unitLabelRef = unitType.unitLabel() ?: unit.takeIf { it.isNotEmpty() }?.let { TextRef.Literal(it) } @@ -106,7 +106,7 @@ fun AdaptiveDoublePreferenceItem( valueRange = doubleKey.min..doubleKey.max, step = step, showValue = true, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, valueFormat = valueFormat, unitLabel = unitLabelRef, asDuration = unitType.isDuration(), diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index f4a11b9cfec5..6ec1c6ffe954 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -22,7 +22,7 @@ import app.aaps.core.ui.compose.rangeText import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.stringResourceOrNull import app.aaps.core.ui.compose.unitLabel -import app.aaps.core.ui.compose.valueFormatResId +import app.aaps.core.ui.compose.valueFormat /** * Composable int preference for use inside card sections. @@ -55,7 +55,7 @@ fun AdaptiveIntPreferenceItem( // Get formatting info from UnitType val unitType = intKey.unitType - val valueFormatResId = unitType.valueFormatResId() + val valueFormatRef = unitType.valueFormat() // Get unit label from UnitType (for dialog input suffix) val unitLabelRef = unitType.unitLabel() ?: unit.takeIf { it.isNotEmpty() }?.let { TextRef.Literal(it) } @@ -98,7 +98,7 @@ fun AdaptiveIntPreferenceItem( valueRange = intKey.min.toDouble()..intKey.max.toDouble(), step = 1.0, showValue = true, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = true, valueFormat = NumberFormat.INTEGER, unitLabel = unitLabelRef, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt index 8d02d1827d08..6c8fa8dd210c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt @@ -85,10 +85,10 @@ fun AdaptivePasswordPreferenceItem( ) if (showDialog) { - val dontMatchMsg = stringResource(if (isPin) R.string.pin_dont_match else R.string.passwords_dont_match) - val setMsg = stringResource(if (isPin) R.string.pin_set else R.string.password_set) - val clearedMsg = stringResource(if (isPin) R.string.pin_cleared else R.string.password_cleared) - val notChangedMsg = stringResource(if (isPin) R.string.pin_not_changed else R.string.password_not_changed) + val dontMatchMsg = stringResource(if (isPin) UiStrings.pin_dont_match else UiStrings.passwords_dont_match) + val setMsg = stringResource(if (isPin) UiStrings.pin_set else UiStrings.password_set) + val clearedMsg = stringResource(if (isPin) UiStrings.pin_cleared else UiStrings.password_cleared) + val notChangedMsg = stringResource(if (isPin) UiStrings.pin_not_changed else UiStrings.password_not_changed) SetPasswordDialog( title = stringResource(effectiveTitle), diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index 947fa94541d1..02f869fbc04c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -4,6 +4,7 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.ui.UiStrings import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions @@ -69,8 +70,8 @@ fun AdaptiveStringPreferenceItem( } isSecure && value.isEmpty() -> { - val notSetResId = if (stringKey.isPin) R.string.pin_not_set else R.string.password_not_set - { Text(stringResource(effectiveSummary ?: TextRef.AndroidRes(notSetResId))) } + val notSetResId = if (stringKey.isPin) UiStrings.pin_not_set else UiStrings.password_not_set + { Text(stringResource(effectiveSummary ?: notSetResId)) } } value.isNotEmpty() -> { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt index 808a60fe6130..2aff012ef641 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt @@ -127,7 +127,7 @@ internal fun ClickablePreferenceCategoryHeader( if (collapsible) { Icon( imageVector = Icons.Default.ExpandMore, - contentDescription = stringResource(if (expanded) app.aaps.core.ui.R.string.collapse else app.aaps.core.ui.R.string.expand), + contentDescription = stringResource(if (expanded) UiStrings.collapse else UiStrings.expand), modifier = Modifier .size(theme.expandIconSize) .graphicsLayer { rotationZ = rotationAngle.value } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt index fb0ae0d2c98e..b06a6dd1f632 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,7 +15,7 @@ import app.aaps.core.ui.R internal fun CollapsibleCardSectionContentPreview() { PreviewTheme { CollapsibleCardSectionContent( - title = TextRef.AndroidRes(R.string.configbuilder_insulin), + title = UiStrings.configbuilder_insulin, expanded = true, onToggle = {} ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt index 9c633307e74c..caadc5406aff 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt @@ -90,7 +90,7 @@ fun PluginPreferencesScreen( IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(app.aaps.core.ui.R.string.back) + contentDescription = stringResource(UiStrings.back) ) } } @@ -105,7 +105,7 @@ fun PluginPreferencesScreen( contentAlignment = Alignment.Center ) { Text( - text = stringResource(app.aaps.core.ui.R.string.no_compose_preferences), + text = stringResource(UiStrings.no_compose_preferences), style = MaterialTheme.typography.bodyMedium ) } @@ -147,7 +147,7 @@ private fun SinglePluginPreferencesRenderer( IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(app.aaps.core.ui.R.string.back) + contentDescription = stringResource(UiStrings.back) ) } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt index 73f0b085c9b6..cebd7f9f5991 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt @@ -41,7 +41,7 @@ fun PreferenceSliderWithButtons( valueRange: ClosedFloatingPointRange, step: Double = 0.1, showValue: Boolean = false, - valueFormatResId: Int? = null, + valueFormatRef: TextRef? = null, formatAsInt: Boolean = false, valueFormat: NumberFormat = NumberFormat.DECIMAL_1, unitLabel: TextRef? = null, @@ -59,7 +59,7 @@ fun PreferenceSliderWithButtons( valueRange = valueRange, step = step, showValue = showValue, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = formatAsInt, valueFormat = valueFormat, unitLabel = unitLabel, @@ -77,7 +77,7 @@ fun PreferenceSliderWithButtons( val displayText = if (showValue) formatSliderDisplayValue( value = value, unitLabel = unitLabel, - valueFormatResId = valueFormatResId, + valueFormatRef = valueFormatRef, formatAsInt = formatAsInt, valueFormat = valueFormat, asDuration = asDuration diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt index 8f7774c4aa1b..766d0bd343a5 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.preference +import app.aaps.core.ui.UiStrings import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.keys.interfaces.PreferenceItem import app.aaps.core.keys.interfaces.PreferenceKey @@ -11,7 +12,7 @@ import app.aaps.core.keys.interfaces.TextRef * Content is auto-generated from items using AdaptivePreferenceList. * * The constructor still takes plain resource ids, because roughly fifty plugin call sites build - * these with `titleResId = R.string.x`. The [title] and [summary] properties wrap them, so the + * these with `titleResId = UiStrings.x`. The [title] and [summary] properties wrap them, so the * rendering code only ever deals with [TextRef], the same as it does for [PreferenceKey]. * * @param key Unique key for this subscreen diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt index 757237989b9f..dd0825c931a8 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt @@ -65,7 +65,7 @@ fun BleScanStep( WizardStepLayout( scrollable = false, secondaryButton = WizardButton( - text = stringResource(app.aaps.core.ui.R.string.cancel), + text = stringResource(UiStrings.cancel), onClick = onCancel ) ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt index f79e33c72ca0..c7d0eaaa11cb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import app.aaps.core.ui.UiStrings import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.ui.R diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt index 4eb65c088931..512cb8d53a68 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.elements -import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.ui.UiStrings import app.aaps.core.ui.R import java.util.Calendar import java.util.Date @@ -14,7 +15,7 @@ open class WeekDay { return calendarInts[ordinal] } - @get:StringRes val shortName: Int + val shortName: TextRef get() = shortNames[ordinal] companion object { @@ -28,14 +29,14 @@ open class WeekDay { Calendar.SATURDAY, Calendar.SUNDAY ) - private val shortNames = intArrayOf( - R.string.weekday_monday_short, - R.string.weekday_tuesday_short, - R.string.weekday_wednesday_short, - R.string.weekday_thursday_short, - R.string.weekday_friday_short, - R.string.weekday_saturday_short, - R.string.weekday_sunday_short + private val shortNames = arrayOf( + UiStrings.weekday_monday_short, + UiStrings.weekday_tuesday_short, + UiStrings.weekday_wednesday_short, + UiStrings.weekday_thursday_short, + UiStrings.weekday_friday_short, + UiStrings.weekday_saturday_short, + UiStrings.weekday_sunday_short ) fun fromCalendarInt(day: Int): DayOfWeek { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt index 3301534ed65d..00b5fa7f80b8 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/utils/HardLimitsImplTest.kt @@ -42,7 +42,7 @@ class HardLimitsImplTest : TestBase() { whenever(persistenceLayer.insertPumpTherapyEventIfNewByTimestamp(any(), any(), any(), any(), any(), any())).thenReturn(PersistenceLayer.TransactionResult()) } whenever(rh.gs(any())).thenReturn("") - whenever(rh.gs(any(), any())).thenReturn("") + whenever(rh.gs(any(), any())).thenReturn("") } @Test diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/elements/MiscElements.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/elements/MiscElements.kt index a7a5fe35b455..2dbf6991177a 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/elements/MiscElements.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/elements/MiscElements.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.automation.compose.elements +import app.aaps.core.ui.compose.stringResource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index 020edd3cfe15..b20a77f4c740 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -93,7 +93,7 @@ import app.aaps.core.objects.runningMode.RunningModeGuard import app.aaps.core.objects.wizard.QuickWizard import app.aaps.core.objects.wizard.QuickWizardEntry import app.aaps.core.objects.wizard.QuickWizardMode -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.ui.compose.DarkGeneralColors import app.aaps.core.ui.compose.LightGeneralColors import app.aaps.plugins.sync.R @@ -841,7 +841,7 @@ class DataHandlerMobile @Inject constructor( */ private fun relayReason(progress: ActionProgress): String = when (progress) { is ActionProgress.Unconfirmed -> rh.gs(app.aaps.core.ui.R.string.clientcontrol_unconfirmed_wear) - is ActionProgress.Rejected -> progress.detail ?: rh.gs(progress.reason.failTextResId()) + is ActionProgress.Rejected -> progress.detail ?: rh.gs(progress.reason.failText()) else -> rh.gs(app.aaps.core.ui.R.string.error) } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt index d980ce050031..9973bb14d3b4 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.wear.wearintegration +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.RM import app.aaps.core.data.model.TT @@ -102,6 +103,7 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { sut.automation = automation // Confirm-title + error-title + client-reject string all go through the single-arg gs(). whenever(rh.gs(any())).thenReturn("CONFIRM") + whenever(rh.gs(any())).thenReturn("CONFIRM") whenever(activePlugin.activePump).thenReturn(pump) whenever(pump.isInitialized()).thenReturn(true) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogViewModel.kt index 33a83a1935ea..1f87f52cae91 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/carbsDialog/CarbsDialogViewModel.kt @@ -12,7 +12,7 @@ import app.aaps.core.interfaces.automation.Automation import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.ConstraintsChecker @@ -277,7 +277,7 @@ class CarbsDialogViewModel @Inject constructor( when (val prepared = batchExecutor.prepare(actions, Sources.CarbDialog, rh.gs(app.aaps.core.ui.R.string.carbs))) { is ActionProgress.Prepared -> _sideEffect.tryEmit(SideEffect.ShowConfirmation(prepared.id, prepared.lines)) is ActionProgress.Rejected -> when (prepared.reason) { - FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.carbs), message = rh.gs(prepared.reason.failTextResId()))) + FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.carbs), message = rh.gs(prepared.reason.failText()))) // No-op (e.g. nothing left to remove after a COB-shrink between open and confirm): neutral message, NOT the bolus-error alarm. FailureReason.NoAction -> _sideEffect.tryEmit(SideEffect.ShowNoActionDialog) else -> prepared.detail?.let { detail -> @@ -303,7 +303,7 @@ class CarbsDialogViewModel @Inject constructor( // NoPendingBolus, …) → the master's detail. Unconfirmed (state unknown) rides the round-trip's app-level modal. if (result is ActionProgress.Rejected) { if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.carbs), message = rh.gs(result.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.carbs), message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.carbs), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/clientcontrol/ClientControlPendingDialog.kt b/ui/src/main/kotlin/app/aaps/ui/compose/clientcontrol/ClientControlPendingDialog.kt index b17bab66fcb2..0382e19527b6 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/clientcontrol/ClientControlPendingDialog.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/clientcontrol/ClientControlPendingDialog.kt @@ -1,5 +1,7 @@ package app.aaps.ui.compose.clientcontrol +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,7 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import app.aaps.core.interfaces.clientcontrol.ActionProgress import app.aaps.core.interfaces.clientcontrol.FailureReason -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.PendingAction import app.aaps.ui.R import app.aaps.core.ui.R as CoreUiR @@ -90,4 +92,4 @@ fun ClientControlPendingDialog( /** Localized message for a [FailureReason] code (the shared mapping; unknown codes from a newer master → generic). */ @Composable -private fun reasonText(reason: FailureReason): String = stringResource(reason.failTextResId()) +private fun reasonText(reason: FailureReason): String = stringResource(reason.failText()) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationModels.kt b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationModels.kt index 81e148a069a6..199063cd9ef8 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationModels.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationModels.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.configuration +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.data.plugin.PluginType @@ -12,7 +13,7 @@ import app.aaps.core.ui.compose.ConfigPluginUiModel @Immutable data class ConfigCategoryUiModel( val type: PluginType, - val titleRes: Int, + val titleRes: TextRef, val plugins: List, val isMultiSelect: Boolean, val subtitle: String, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationScreen.kt index e4860f4fbb62..538dab3f9207 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.configuration +import app.aaps.core.ui.compose.stringResource import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationViewModel.kt index f8628d2c6965..2021643ae828 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/ConfigurationViewModel.kt @@ -15,7 +15,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.ConfigPluginUiModel -import app.aaps.core.ui.compose.pluginCategoryTitleRes +import app.aaps.core.ui.compose.pluginCategoryTitle import app.aaps.ui.plugin.HardwarePumpConfirmation import app.aaps.ui.plugin.PluginSwitchConfirmation import app.aaps.ui.plugin.PluginSwitchDialogs @@ -132,7 +132,7 @@ class ConfigurationViewModel @Inject constructor( fun addCategory(type: PluginType) { val plugins = activePlugin.getSpecificPluginsVisibleInList(type) if (plugins.isEmpty()) return - val titleRes = pluginCategoryTitleRes(type) + val titleRes = pluginCategoryTitle(type) val isMultiSelect = isMultiSelect(type) val pluginModels = plugins.map { plugin -> diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/PluginCategoryScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/PluginCategoryScreen.kt index fe7cef8f34a4..71a277eeaf51 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/configuration/PluginCategoryScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/configuration/PluginCategoryScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.configuration +import app.aaps.core.ui.compose.stringResource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogViewModel.kt index 3f0c38a748f6..e5acbbbcbbb0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/extendedBolusDialog/ExtendedBolusDialogViewModel.kt @@ -8,7 +8,7 @@ import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.configuration.Config @@ -125,7 +125,7 @@ class ExtendedBolusDialogViewModel @Inject constructor( is ActionProgress.Prepared -> _sideEffect.tryEmit(SideEffect.ShowConfirmation(prepared.id, prepared.lines)) // Offline block (and a master-local failure) surface here; a client round-trip failure already showed on the modal. is ActionProgress.Rejected -> - if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = rh.gs(prepared.reason.failTextResId()))) + if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = rh.gs(prepared.reason.failText()))) else prepared.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) @@ -144,7 +144,7 @@ class ExtendedBolusDialogViewModel @Inject constructor( appScope.launch { val result = batchExecutor.commit(bolusId, Sources.ExtendedBolusDialog, rh.gs(app.aaps.core.ui.R.string.extended_bolus), pumpDirect = true) if (result is ActionProgress.Rejected) - if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = rh.gs(result.reason.failTextResId()))) + if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.extended_bolus), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModel.kt index 95d252c0465f..dbadd303c605 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModel.kt @@ -15,7 +15,7 @@ import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.bolus.WizardBolusExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.db.PersistenceLayer @@ -436,7 +436,7 @@ class FillDialogViewModel @Inject constructor( else -> { // Rejected, or a non-terminal value that can never become one here (nothing further awaits it). - val detail = (outcome as? ActionProgress.Rejected)?.let { rh.gs(it.reason.failTextResId()) } + val detail = (outcome as? ActionProgress.Rejected)?.let { rh.gs(it.reason.failText()) } aapsLogger.warn(LTag.UI, "Fill insulin activation failed: $outcome") reportAfterClose( CoreUiR.string.activate_insulin, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogViewModel.kt index acb353f0c314..b631ff192ba9 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinDialog/InsulinDialogViewModel.kt @@ -34,7 +34,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.runningMode.PumpCommandGate -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow @@ -241,7 +241,7 @@ class InsulinDialogViewModel @Inject constructor( // Offline block (and a master-local failure) surface here; a client round-trip failure already showed // on the app-level modal, so only re-surface NotReachable or a master-side detail message. is ActionProgress.Rejected -> when (prepared.reason) { - FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(prepared.reason.failTextResId()))) + FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(prepared.reason.failText()))) // No-op after caps (e.g. the bolus was constraint-capped to 0): neutral message, NOT the bolus-error alarm. FailureReason.NoAction -> _sideEffect.tryEmit(SideEffect.ShowNoActionDialog) else -> prepared.detail?.let { detail -> @@ -267,7 +267,7 @@ class InsulinDialogViewModel @Inject constructor( // NoPendingBolus, …) → the master's detail. Unconfirmed (state unknown) rides the round-trip's app-level modal. if (result is ActionProgress.Rejected) { if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(result.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModel.kt index 22e61b11c4ee..2fbf9e713d90 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModel.kt @@ -11,7 +11,7 @@ import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer @@ -510,7 +510,7 @@ class InsulinManagementViewModel @Inject constructor( // Offline block (and a master-local failure, e.g. no active profile) surface here; a client round-trip // failure already showed on the app-level modal. is ActionProgress.Rejected -> - if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) showSnackbar(rh.gs(prepared.reason.failTextResId())) + if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) showSnackbar(rh.gs(prepared.reason.failText())) else prepared.detail?.let { showSnackbar(it) } else -> Unit // Unconfirmed → app-level modal @@ -531,7 +531,7 @@ class InsulinManagementViewModel @Inject constructor( refreshData() } - result is ActionProgress.Rejected && (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) -> showSnackbar(rh.gs(result.reason.failTextResId())) + result is ActionProgress.Rejected && (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) -> showSnackbar(rh.gs(result.reason.failText())) result is ActionProgress.Rejected -> result.detail?.let { showSnackbar(it) } } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt index 912349badc15..feb37b693b18 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt @@ -19,7 +19,7 @@ import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.bolus.WizardExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress import app.aaps.core.interfaces.clientcontrol.FailureReason -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.configuration.ExternalOptions import app.aaps.core.interfaces.constraints.ConstraintsChecker @@ -479,7 +479,7 @@ class MainViewModel @Inject constructor( // A master-local compute failure (no modal) or a client offline pre-check surfaces here; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = entry.buttonText(), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = entry.buttonText(), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit // Unconfirmed → app modal } @@ -509,7 +509,7 @@ class MainViewModel @Inject constructor( // A master-local failure (no modal) or a client offline pre-check surfaces here; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = entry.buttonText(), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = entry.buttonText(), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit // Unconfirmed → app modal } @@ -673,7 +673,7 @@ class MainViewModel @Inject constructor( is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit } @@ -691,7 +691,7 @@ class MainViewModel @Inject constructor( is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = label, message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = label, message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit } @@ -776,7 +776,7 @@ class MainViewModel @Inject constructor( is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = title, message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = title, message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt index 9679ca67cb53..31c1253c9101 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt @@ -14,7 +14,7 @@ import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress import app.aaps.core.interfaces.clientcontrol.FailureReason -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.db.ProcessedTbrEbData @@ -221,7 +221,7 @@ class ManageViewModel @Inject constructor( is ActionProgress.Prepared -> _sideEffect.tryEmit(SideEffect.ShowConfirmation(elementType, prepared.id, prepared.lines, label)) // Offline block (and a master-local failure) surface here; a client round-trip failure already showed on the modal. is ActionProgress.Rejected -> - if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(prepared.reason.failTextResId()))) + if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(prepared.reason.failText()))) else prepared.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = label, message = detail)) else _sideEffect.tryEmit(SideEffect.ShowError(elementType, detail)) @@ -238,7 +238,7 @@ class ManageViewModel @Inject constructor( // NoPendingBolus (a double-tapped dialog already consumed it) stays silent — the cancel ran once. val result = batchExecutor.commit(bolusId, Sources.Actions, label, pumpDirect = true) if (result is ActionProgress.Rejected) - if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(result.reason.failTextResId()))) + if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = label, message = detail)) else _sideEffect.tryEmit(SideEffect.ShowError(elementType, detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt index 41515e84d45b..4b16aa6a7a47 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt @@ -42,7 +42,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.toPureProfile import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.R -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.ui.compose.ScreenMode import app.aaps.core.ui.compose.icons.IcProfile import dagger.hilt.android.lifecycle.HiltViewModel @@ -655,7 +655,7 @@ class ProfileManagementViewModel @Inject constructor( is ActionProgress.Rejected -> if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(result.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> rxBus.send(EventShowDialog.Ok(title = label, message = detail)) } @@ -672,7 +672,7 @@ class ProfileManagementViewModel @Inject constructor( // Master-local pre-check failure, or a client offline; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> { if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = label, message = rh.gs(prepared.reason.failText()))) else prepared.detail?.let { detail -> rxBus.send(EventShowDialog.Ok(title = label, message = detail)) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt index e096e5408df6..0d9c072fd2e2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.quickLaunch +import app.aaps.core.ui.compose.stringResource import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigViewModel.kt index b632f35c3ebf..28c360550664 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchConfigViewModel.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.quickLaunch +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel @@ -13,7 +14,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.objects.extensions.profileNames import app.aaps.core.objects.wizard.QuickWizard -import app.aaps.core.ui.compose.pluginCategoryTitleRes +import app.aaps.core.ui.compose.pluginCategoryTitle import app.aaps.core.interfaces.scenes.SceneStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -37,7 +38,7 @@ data class QuickLaunchConfigUiState( @Immutable data class PluginGroup( val pluginType: PluginType, - val labelResId: Int, + val labelResId: TextRef, val items: List ) @@ -197,7 +198,7 @@ class QuickLaunchConfigViewModel @Inject constructor( .filter { it.pluginDescription.mainType == type } .map { plugin -> resolver.resolvePluginItem(plugin) } .filter { actionKey(it.action) !in selectedSet } - if (items.isNotEmpty()) PluginGroup(type, pluginCategoryTitleRes(type), items) else null + if (items.isNotEmpty()) PluginGroup(type, pluginCategoryTitle(type), items) else null } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModel.kt index 93c208dff7b0..d3463eeaba5d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModel.kt @@ -13,7 +13,7 @@ import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer @@ -169,7 +169,7 @@ class RunningModeManagementViewModel @Inject constructor( // Master-local validation failure, or a client offline; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> { if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowSnackbar(prepared.detail ?: rh.gs(prepared.reason.failTextResId()), EventShowSnackbar.Type.Error)) + rxBus.send(EventShowSnackbar(prepared.detail ?: rh.gs(prepared.reason.failText()), EventShowSnackbar.Type.Error)) } else -> Unit // Unconfirmed → handled by the app-level pending modal diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt index 664ad48dd5ff..033194609211 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt @@ -9,7 +9,7 @@ import app.aaps.core.data.model.Scene import app.aaps.core.data.model.SceneAction import app.aaps.core.data.model.SceneEndAction import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.profile.ProfileRepository @@ -220,7 +220,7 @@ class SceneListViewModel @Inject constructor( DialogState.ConfirmActivation(scene, prepared.lines.map { it.text }, conflicts, prepared.id) is ActionProgress.Rejected -> - _dialogState.value = DialogState.ValidationError(prepared.detail ?: rh.gs(prepared.reason.failTextResId())) + _dialogState.value = DialogState.ValidationError(prepared.detail ?: rh.gs(prepared.reason.failText())) else -> Unit // Unconfirmed → the round-trip's app-level modal already showed it } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/DurationStep.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/DurationStep.kt index 3e16758a17a9..5b65e456525f 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/DurationStep.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/wizard/DurationStep.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.scenes.wizard +import app.aaps.core.keys.interfaces.TextRef import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -48,7 +49,7 @@ internal fun DurationStep( onValueChange = { onSetDuration(it.toInt()) }, valueRange = Constants.SCENE_DURATION, step = 5.0, - valueFormatResId = R.string.mins, + valueFormatRef = TextRef.AndroidRes(R.string.mins), formatAsInt = true, displayValue = when { state.durationMinutes == 0 -> stringResource(R.string.scene_duration_indefinite) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogViewModel.kt index 1f5134f1077c..eadcdb57deb7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempBasalDialog/TempBasalDialogViewModel.kt @@ -9,7 +9,7 @@ import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.di.ApplicationScope @@ -125,7 +125,7 @@ class TempBasalDialogViewModel @Inject constructor( is ActionProgress.Prepared -> _sideEffect.tryEmit(SideEffect.ShowConfirmation(prepared.id, prepared.lines)) // Offline block (and a master-local failure) surface here; a client round-trip failure already showed on the modal. is ActionProgress.Rejected -> - if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = rh.gs(prepared.reason.failTextResId()))) + if (prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = rh.gs(prepared.reason.failText()))) else prepared.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) @@ -144,7 +144,7 @@ class TempBasalDialogViewModel @Inject constructor( appScope.launch { val result = batchExecutor.commit(bolusId, Sources.TempBasalDialog, rh.gs(app.aaps.core.ui.R.string.tempbasal_label), pumpDirect = true) if (result is ActionProgress.Rejected) - if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = rh.gs(result.reason.failTextResId()))) + if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModel.kt index c9e9ae9119fd..b420cae00b93 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModel.kt @@ -11,7 +11,7 @@ import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.bolus.BatchAction import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer @@ -748,7 +748,7 @@ class TempTargetManagementViewModel @Inject constructor( // Master-local failure (no modal) or a client offline pre-check; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit } @@ -782,7 +782,7 @@ class TempTargetManagementViewModel @Inject constructor( is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.temporary_target), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogViewModel.kt index bd0d641a8dfa..ee669f288dbb 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentDialog/TreatmentDialogViewModel.kt @@ -23,7 +23,7 @@ import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.interfaces.utils.Round import app.aaps.core.objects.runningMode.PumpCommandGate -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow @@ -133,7 +133,7 @@ class TreatmentDialogViewModel @Inject constructor( when (val prepared = batchExecutor.prepare(actions, Sources.TreatmentDialog, rh.gs(app.aaps.core.ui.R.string.bolus))) { is ActionProgress.Prepared -> _sideEffect.tryEmit(SideEffect.ShowConfirmation(prepared.id, prepared.lines)) is ActionProgress.Rejected -> when (prepared.reason) { - FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(prepared.reason.failTextResId()))) + FailureReason.NotReachable, FailureReason.ControlDisabled -> rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(prepared.reason.failText()))) // No-op after caps (e.g. the bolus was constraint-capped to 0): neutral message, NOT the bolus-error alarm. FailureReason.NoAction -> _sideEffect.tryEmit(SideEffect.ShowNoActionDialog) else -> prepared.detail?.let { detail -> @@ -155,7 +155,7 @@ class TreatmentDialogViewModel @Inject constructor( // NoPendingBolus, …) → the master's detail. Unconfirmed (state unknown) rides the round-trip's app-level modal. if (result is ActionProgress.Rejected) { if (result.reason == FailureReason.NotReachable || result.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(result.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = rh.gs(result.reason.failText()))) else result.detail?.let { detail -> if (config.AAPSCLIENT) rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.bolus), message = detail)) else _sideEffect.tryEmit(SideEffect.ShowDeliveryError(detail)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogViewModel.kt index 2079aaeeae1a..773f61aaa8dc 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/wizardDialog/WizardDialogViewModel.kt @@ -11,7 +11,7 @@ import app.aaps.core.interfaces.automation.Automation import app.aaps.core.interfaces.bolus.WizardBolusExecutor import app.aaps.core.interfaces.bolus.WizardExecutor import app.aaps.core.interfaces.clientcontrol.ActionProgress -import app.aaps.core.ui.clientcontrol.failTextResId +import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.interfaces.clientcontrol.FailureReason import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.ConstraintsChecker @@ -534,7 +534,7 @@ class WizardDialogViewModel @Inject constructor( // Master-local compute failure (no modal) or client offline; a client round-trip failure already showed on the app modal. is ActionProgress.Rejected -> if (!config.AAPSCLIENT || prepared.reason == FailureReason.NotReachable || prepared.reason == FailureReason.ControlDisabled) - rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.boluswizard), message = prepared.detail ?: rh.gs(prepared.reason.failTextResId()))) + rxBus.send(EventShowDialog.Ok(title = rh.gs(app.aaps.core.ui.R.string.boluswizard), message = prepared.detail ?: rh.gs(prepared.reason.failText()))) else -> Unit // Unconfirmed → app modal } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModelTest.kt index c875f3e00438..7717a5ff1500 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/fillDialog/FillDialogViewModelTest.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.fillDialog +import app.aaps.core.keys.interfaces.TextRef import androidx.lifecycle.SavedStateHandle import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.bolus.BatchExecutor @@ -109,6 +110,7 @@ internal class FillDialogViewModelTest { */ private fun stubStrings() { whenever(rh.gs(any())).thenReturn("s") + whenever(rh.gs(any())).thenReturn("s") whenever(rh.gs(any(), anyOrNull())).thenReturn("s") whenever(rh.gs(any(), anyOrNull(), anyOrNull())).thenReturn("s") whenever(rh.gs(CoreUiR.string.insulin_activation_unconfirmed)).thenReturn("UNCONFIRMED") From 2e0dcd0404a8f1c061522abca9920cc3e4b536df Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 13:43:00 +0200 Subject: [PATCH 035/146] :core:ui java cleanup --- core/ui/build.gradle.kts | 1 + .../aaps/core/ui/compose/SliderWithButtons.kt | 6 +- .../app/aaps/core/ui/elements/WeekDay.kt | 33 +++++---- .../app/aaps/core/ui/elements/WeekDayTest.kt | 68 +++++++++++++++++++ 4 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 771241a2ae2c..b10dbcd194ad 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(project(":core:interfaces")) implementation(project(":core:keys")) implementation(project(":core:data")) + implementation(libs.kotlinx.datetime) implementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.tooling) } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt index 1bb87d092e96..212f61b26cf1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt @@ -38,6 +38,8 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.dialogs.ValueInputDialog import kotlinx.coroutines.delay +import kotlin.math.roundToLong +import kotlin.math.pow import kotlin.math.roundToInt /** @@ -266,8 +268,8 @@ internal fun roundToStep(value: Double, step: Double): Double { val scaled = (value / step).roundToInt() * step // Fix floating point precision errors (e.g., 6.1000000000005 -> 6.1) val decimals = step.toString().substringAfter('.', "").length - val factor = Math.pow(10.0, decimals.toDouble()) - return Math.round(scaled * factor) / factor + val factor = 10.0.pow(decimals.toDouble()) + return (scaled * factor).roundToLong() / factor } /** diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt index 512cb8d53a68..aa0ab625a713 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt @@ -2,9 +2,9 @@ package app.aaps.core.ui.elements import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings -import app.aaps.core.ui.R -import java.util.Calendar -import java.util.Date +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Instant open class WeekDay { @@ -20,15 +20,11 @@ open class WeekDay { companion object { - private val calendarInts = intArrayOf( - Calendar.MONDAY, - Calendar.TUESDAY, - Calendar.WEDNESDAY, - Calendar.THURSDAY, - Calendar.FRIDAY, - Calendar.SATURDAY, - Calendar.SUNDAY - ) + // The numbers java.util.Calendar uses: SUNDAY is 1 and SATURDAY is 7, so Monday is 2. + // They are written out rather than taken from Calendar because these values are a + // persisted contract - automation triggers store them and `getSelectedDays()` hands + // them to other modules - so they must not change, and they must not need the JVM. + private val calendarInts = intArrayOf(2, 3, 4, 5, 6, 7, 1) private val shortNames = arrayOf( UiStrings.weekday_monday_short, UiStrings.weekday_tuesday_short, @@ -64,9 +60,18 @@ open class WeekDay { fun isSet(day: DayOfWeek): Boolean = weekdays[day.ordinal] + /** + * Which weekday a moment falls on, in the device's own time zone - an automation set for Monday + * has to mean the user's Monday, not UTC's. + * + * `kotlinx.datetime.DayOfWeek` runs MONDAY..SUNDAY, the same order as [DayOfWeek] here, so the + * ordinal carries across directly and no ISO-number conversion is involved. + */ fun isSet(timestamp: Long): Boolean { - val scheduledDayOfWeek = Calendar.getInstance().also { it.time = Date(timestamp) } - return isSet(DayOfWeek.fromCalendarInt(scheduledDayOfWeek[Calendar.DAY_OF_WEEK])) + val dayOfWeek = Instant.fromEpochMilliseconds(timestamp) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .dayOfWeek + return isSet(DayOfWeek.entries[dayOfWeek.ordinal]) } fun getSelectedDays(): List { diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt new file mode 100644 index 000000000000..c95556ba864f --- /dev/null +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt @@ -0,0 +1,68 @@ +package app.aaps.core.ui.elements + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test +import java.util.Calendar +import java.util.TimeZone + +/** + * Pins the weekday numbering, because getting it wrong is silent and wrong by exactly one day. + * + * `toCalendarInt()` / `getSelectedDays()` hand these numbers to `:plugins:automation` and + * `:plugins:aps`, and automation triggers persist them - so the values are a stored contract, not an + * implementation detail. They used to come from `java.util.Calendar`; they are now written out so + * the class does not need the JVM. This test is what makes that swap safe: it compares the literals + * against the real `Calendar` constants. + */ +class WeekDayTest { + + @Test + fun `calendar ints match java util Calendar exactly`() { + assertThat(WeekDay.DayOfWeek.MONDAY.toCalendarInt()).isEqualTo(Calendar.MONDAY) + assertThat(WeekDay.DayOfWeek.TUESDAY.toCalendarInt()).isEqualTo(Calendar.TUESDAY) + assertThat(WeekDay.DayOfWeek.WEDNESDAY.toCalendarInt()).isEqualTo(Calendar.WEDNESDAY) + assertThat(WeekDay.DayOfWeek.THURSDAY.toCalendarInt()).isEqualTo(Calendar.THURSDAY) + assertThat(WeekDay.DayOfWeek.FRIDAY.toCalendarInt()).isEqualTo(Calendar.FRIDAY) + assertThat(WeekDay.DayOfWeek.SATURDAY.toCalendarInt()).isEqualTo(Calendar.SATURDAY) + assertThat(WeekDay.DayOfWeek.SUNDAY.toCalendarInt()).isEqualTo(Calendar.SUNDAY) + } + + @Test + fun `fromCalendarInt round trips`() { + WeekDay.DayOfWeek.entries.forEach { day -> + assertThat(WeekDay.DayOfWeek.fromCalendarInt(day.toCalendarInt())).isEqualTo(day) + } + } + + @Test + fun `a timestamp resolves to the same weekday as Calendar does`() { + // Compared against Calendar in the default zone rather than asserted as a literal, so the + // test says what it means - "the new implementation agrees with the old one" - and keeps + // saying it wherever it runs. + val day = 24 * 60 * 60 * 1000L + var t = 1_767_225_600_000L // 2026-01-01T00:00:00Z + repeat(14) { + val expected = Calendar.getInstance(TimeZone.getDefault()).also { c -> c.timeInMillis = t } + .get(Calendar.DAY_OF_WEEK) + + val weekDay = WeekDay() + WeekDay.DayOfWeek.entries.forEach { weekDay[it] = false } + weekDay[WeekDay.DayOfWeek.fromCalendarInt(expected)] = true + + assertThat(weekDay.isSet(t)).isTrue() + t += day + } + } + + @Test + fun `only the matching day is set`() { + val monday = 1_767_398_400_000L // 2026-01-03T00:00:00Z, a Saturday in UTC + val weekDay = WeekDay() + val actual = WeekDay.DayOfWeek.entries.filter { day -> + weekDay.setAll(false) + weekDay[day] = true + weekDay.isSet(monday) + } + assertThat(actual).hasSize(1) + } +} From fff1704be5472f78358d2f50a80303dbe8753a49 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 13:57:54 +0200 Subject: [PATCH 036/146] :core:ui dead res imports --- .../aaps/core/keys/interfaces/IntentPreferenceKey.kt | 6 +++--- .../kotlin/app/aaps/core/ui/compose/FormatUtils.kt | 1 - .../aaps/core/ui/compose/ImportSummaryComponents.kt | 2 +- .../kotlin/app/aaps/core/ui/compose/InsulinSelector.kt | 1 - .../kotlin/app/aaps/core/ui/compose/NumberInputRow.kt | 1 - .../kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt | 1 - .../app/aaps/core/ui/compose/SliderWithButtons.kt | 1 - .../ui/compose/dialogs/ElementConfirmationDialog.kt | 1 - .../core/ui/compose/dialogs/QueryPasswordDialog.kt | 1 - .../aaps/core/ui/compose/dialogs/SetPasswordDialog.kt | 1 - .../aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt | 1 - .../aaps/core/ui/compose/dialogs/ValueInputDialog.kt | 1 - .../aaps/core/ui/compose/pickers/WeekDaySelector.kt | 1 - .../ui/compose/preference/AdaptiveDoublePreference.kt | 1 - .../ui/compose/preference/AdaptiveIntPreference.kt | 1 - .../ui/compose/preference/AdaptiveIntentPreference.kt | 10 +++++----- .../ui/compose/preference/AdaptiveListPreference.kt | 1 - .../compose/preference/AdaptivePasswordPreference.kt | 1 - .../ui/compose/preference/AdaptiveStringPreference.kt | 1 - .../ui/compose/preference/AdaptiveSwitchPreference.kt | 8 ++++---- .../compose/preference/AdaptiveUnitDoublePreference.kt | 1 - .../preference/ClickablePreferenceCategoryHeader.kt | 1 - .../ui/compose/preference/InlinePreferenceItems.kt | 1 - .../aaps/core/ui/compose/preference/ListPreference.kt | 2 +- .../ui/compose/preference/PluginPreferencesScreen.kt | 1 - .../app/aaps/core/ui/compose/preference/SyncBadge.kt | 1 - .../core/ui/compose/preference/TextFieldPreference.kt | 4 ++-- .../app/aaps/core/ui/compose/pump/BleScanStep.kt | 1 - 28 files changed, 16 insertions(+), 38 deletions(-) diff --git a/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt index 7c92bc3a1a40..cbbe34b4497f 100644 --- a/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt +++ b/core/keys/src/commonMain/kotlin/app/aaps/core/keys/interfaces/IntentPreferenceKey.kt @@ -10,11 +10,11 @@ interface IntentPreferenceKey : PreferenceKey { get() = null /** - * String resource ID for confirmation dialog message. + * Confirmation dialog message. * When set, clicking this preference shows an OK/Cancel dialog before executing onClick. - * The dialog title uses [titleResId]. + * The dialog title uses [title]. */ - val confirmationMessageResId: Int? + val confirmationMessage: TextRef? get() = null /** diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt index 6c322d63c179..e34006b0d2a3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import app.aaps.core.data.format.NumberFormat diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt index 356fddf8bd22..30f2f27fe49c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt @@ -151,7 +151,7 @@ private fun ImportDetailsDialog( }, confirmButton = { TextButton(onClick = onDismiss) { - Text(stringResource(android.R.string.ok)) + Text(stringResource(UiStrings.ok)) } }, properties = androidx.compose.ui.window.DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt index ea8b5470746f..206e6fd71d4b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt @@ -1,7 +1,6 @@ package app.aaps.core.ui.compose import app.aaps.core.keys.interfaces.TextRef -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.DropdownMenuItem diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index e5a676c190b6..cb72131c43d6 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt index 9e05f0710793..b5d8898917fb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose -import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt index 212f61b26cf1..0b2e4e4cfc82 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose -import androidx.compose.ui.res.stringResource import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt index 7d10cd318dd0..3e44a2492eb7 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt @@ -1,7 +1,6 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResourceOrNull -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt index eda527822146..7e799c196dc3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.dialogs -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt index 7a25a210135c..5d059c6f5624 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.dialogs -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt index 89db88ce6ca8..adf9abfbeca3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.dialogs -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt index 8460f6b2c5d6..a87f3c5904d0 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.dialogs -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt index 01726cb7042e..464b4ce06eec 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.pickers -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index c93f8a8ff9ea..6814955e3974 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index 6ec1c6ffe954..96bd22ec838c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index db23bcd0b5ef..5589d61d1c15 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -45,14 +45,14 @@ fun AdaptiveIntentPreferenceItem( if (!visibility.visible) return - // Show confirmation dialog when confirmationMessageResId is set on the key - val confirmationResId = intentKey.confirmationMessageResId + // Show confirmation dialog when confirmationMessage is set on the key + val confirmation = intentKey.confirmationMessage var showConfirmation by remember { mutableStateOf(false) } - if (showConfirmation && confirmationResId != null) { + if (showConfirmation && confirmation != null) { OkCancelDialog( title = stringResource(effectiveTitle), - message = stringResource(confirmationResId), + message = stringResource(confirmation), onConfirm = { onClick() showConfirmation = false @@ -61,7 +61,7 @@ fun AdaptiveIntentPreferenceItem( ) } - val effectiveOnClick = if (confirmationResId != null) { + val effectiveOnClick = if (confirmation != null) { { showConfirmation = true } } else { onClick diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt index 6e21b24832a3..599836dd8a92 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt index 6c8fa8dd210c..95f66d54ec30 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index 02f869fbc04c..1e3a9ee103ec 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -5,7 +5,6 @@ package app.aaps.core.ui.compose.preference import app.aaps.core.ui.UiStrings -import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.OutlinedTextField diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt index b7bb834fe5db..fa7e21909ebe 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt @@ -34,8 +34,8 @@ fun AdaptiveSwitchPreferenceItem( booleanKey: BooleanPreferenceKey, title: TextRef? = null, summary: TextRef? = null, - summaryOnResId: Int? = null, - summaryOffResId: Int? = null, + summaryOn: TextRef? = null, + summaryOff: TextRef? = null, visibilityContext: VisibilityContext? = null ) { val effectiveTitle = title ?: booleanKey.title @@ -55,8 +55,8 @@ fun AdaptiveSwitchPreferenceItem( var guardMessage by remember { mutableStateOf(null) } val summary: @Composable (() -> Unit)? = when { - summaryOnResId != null && summaryOffResId != null -> { - { Text(stringResource(if (state.value) summaryOnResId else summaryOffResId)) } + summaryOn != null && summaryOff != null -> { + { Text(stringResource(if (state.value) summaryOn else summaryOff)) } } effectiveSummary != null -> { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index 71495ec95e18..3b93b2e38b5c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt index 2aff012ef641..9f3d1c11d923 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt @@ -17,7 +17,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt index fe1fcf8e1255..e4fb839efc8a 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt index fe5337b374d9..65d0a0bb1f64 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt @@ -117,7 +117,7 @@ fun ListPreference( title = title, buttons = { TextButton(onClick = { openSelector = false }) { - Text(text = stringResource(android.R.string.cancel)) + Text(text = stringResource(UiStrings.cancel)) } }, ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt index caadc5406aff..1f8b2d062726 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.activity.compose.BackHandler diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt index 2212009c3dff..57f6040535f4 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt index 98a99b26b070..433887790ec3 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt @@ -124,9 +124,9 @@ fun TextFieldPreference( title = title, buttons = { TextButton(onClick = { openDialog = false }) { - Text(text = stringResource(android.R.string.cancel)) + Text(text = stringResource(UiStrings.cancel)) } - TextButton(onClick = onOk) { Text(text = stringResource(android.R.string.ok)) } + TextButton(onClick = onOk) { Text(text = stringResource(UiStrings.ok)) } }, ) { val focusRequester = remember { FocusRequester() } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt index dd0825c931a8..313424ca03a7 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.pump -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.clickable From 1d8dfb45d2019fd9a2a567f0239a5240e4e4a21a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 14:37:27 +0200 Subject: [PATCH 037/146] :core:ui compose paths --- .../ui/compose/icons/library/IcChildBack.kt | 6 +- .../ui/compose/icons/library/IcChildFront.kt | 6 +- .../ui/compose/icons/library/IcManBack.kt | 6 +- .../ui/compose/icons/library/IcManFront.kt | 6 +- .../ui/compose/icons/library/IcWomanBack.kt | 6 +- .../ui/compose/icons/library/IcWomanFront.kt | 6 +- .../core/ui/compose/siteRotation/BodyType.kt | 2 +- .../core/ui/compose/siteRotation/BodyView.kt | 30 ++++---- .../siteRotation/BodyViewContainsPointTest.kt | 70 +++++++++++++++++++ 9 files changed, 107 insertions(+), 31 deletions(-) create mode 100644 core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt index 10a7e587f7b9..dfdb5281ed8c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -191,7 +191,7 @@ object ChildBackPaths { ) val zones: List> by lazy { pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt index 5e656705ae28..f5b8636b2ce7 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -203,7 +203,7 @@ object ChildFrontPaths { ) val zones: List> by lazy { pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt index 96ce1342b3f1..c5634adecc05 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -197,7 +197,7 @@ object ManBackPaths { ) val zones: List> by lazy { pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt index eba4776ad89c..5702bae75e25 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -204,6 +204,6 @@ object ManFrontPaths { Location.FRONT_LEFT_UPPER_CHEST to "M33.046,25.581 a3.989,2.16 -7.0 1,0 -7.978,0 a3.989,2.16 -7.0 1,0 7.978,0z", ) val zones: List> = pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt index 0904d193da1b..4774de6efd81 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -205,7 +205,7 @@ object WomanBackPaths { ) val zones: List> by lazy { pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt index 95e153c94eab..800708e00c27 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.icons.library -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap @@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp -import androidx.core.graphics.PathParser +import androidx.compose.ui.graphics.vector.PathParser import app.aaps.core.data.model.TE.Location /** @@ -204,7 +204,7 @@ object WomanFrontPaths { ) val zones: List> by lazy { pathData.map { (location, svgData) -> - location to PathParser.createPathFromPathData(svgData) + location to PathParser().parsePathString(svgData).toPath() } } } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt index 3ece1461064e..399d8d06afcb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.siteRotation -import android.graphics.Path +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.data.model.TE import app.aaps.core.ui.compose.icons.library.ChildBack diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt index d167440a1acf..e2e14bd89f2d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt @@ -1,7 +1,5 @@ package app.aaps.core.ui.compose.siteRotation -import android.graphics.Path -import android.graphics.Region import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.BoxWithConstraints @@ -12,14 +10,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathOperation import androidx.compose.ui.graphics.Matrix -import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import app.aaps.core.data.model.TE -import androidx.compose.ui.graphics.Path as ComposePath @Composable fun BodyView( @@ -87,8 +86,8 @@ fun BodyView( zones.forEach { (location, path) -> if (showLocation(location)) { - val originalComposePath = path.asComposePath() - val transformedPath = ComposePath().apply { + val originalComposePath = path + val transformedPath = Path().apply { addPath(originalComposePath) transform(matrix) } @@ -109,13 +108,20 @@ fun BodyView( } } -// Workaround to manage click, was not able to make composePath.contains( ... ) working +/** + * Whether a tap landed inside a body zone. + * + * The original used `android.graphics.Region`, with a note that `composePath.contains(...)` could + * not be made to work. That note was right, and so is the reason: Compose's `Path` has no point + * test. Intersecting with a tiny rectangle is the portable way to ask the same question, and unlike + * the `Region` version it does not round the tap to whole pixels - `Region` works in integers, so + * the old code truncated the coordinates before testing. + */ fun Path.containsPoint(x: Float, y: Float): Boolean { - val region = Region() - val bounds = android.graphics.RectF() - computeBounds(bounds, true) - region.setPath(this, Region(bounds.left.toInt(), bounds.top.toInt(), bounds.right.toInt(), bounds.bottom.toInt())) - return region.contains(x.toInt(), y.toInt()) + val probe = Path().apply { addRect(Rect(x - 0.5f, y - 0.5f, x + 0.5f, y + 0.5f)) } + val hit = Path() + hit.op(this, probe, PathOperation.Intersect) + return !hit.isEmpty } internal fun computeZoneColors( diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt new file mode 100644 index 000000000000..c40b5ff93597 --- /dev/null +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt @@ -0,0 +1,70 @@ +package app.aaps.core.ui.compose.siteRotation + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +/** + * Pins the body-diagram hit test, which decides which site a tap selects. + * + * `containsPoint` used to be built on `android.graphics.Region`; it is now a Compose `Path` + * intersection so the code can leave Android. A hit test that is subtly wrong does not crash and + * does not fail a build - it just selects the wrong infusion site, or none - so it is worth pinning + * rather than eyeballing. + * + * Robolectric because `androidx.compose.ui.graphics.Path` needs a real graphics implementation. + */ +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [35]) +class BodyViewContainsPointTest { + + private fun square(left: Float, top: Float, right: Float, bottom: Float) = + Path().apply { addRect(Rect(left, top, right, bottom)) } + + @Test + fun `a point well inside is a hit`() { + val zone = square(10f, 10f, 20f, 20f) + assertThat(zone.containsPoint(15f, 15f)).isTrue() + assertThat(zone.containsPoint(11f, 11f)).isTrue() + assertThat(zone.containsPoint(19f, 19f)).isTrue() + } + + @Test + fun `a point outside is not a hit`() { + val zone = square(10f, 10f, 20f, 20f) + assertThat(zone.containsPoint(5f, 15f)).isFalse() + assertThat(zone.containsPoint(25f, 15f)).isFalse() + assertThat(zone.containsPoint(15f, 5f)).isFalse() + assertThat(zone.containsPoint(15f, 25f)).isFalse() + } + + @Test + fun `a point far away is not a hit`() { + val zone = square(10f, 10f, 20f, 20f) + assertThat(zone.containsPoint(0f, 0f)).isFalse() + assertThat(zone.containsPoint(1000f, 1000f)).isFalse() + } + + @Test + fun `neighbouring zones do not both claim the same tap`() { + // The body diagram is a list of adjacent zones and the first hit wins, so two zones + // answering true for one tap would make the selection depend on list order. + val left = square(0f, 0f, 10f, 10f) + val right = square(20f, 0f, 30f, 10f) + assertThat(left.containsPoint(5f, 5f)).isTrue() + assertThat(right.containsPoint(5f, 5f)).isFalse() + assertThat(left.containsPoint(25f, 5f)).isFalse() + assertThat(right.containsPoint(25f, 5f)).isTrue() + } + + @Test + fun `an empty path never claims a tap`() { + assertThat(Path().containsPoint(0f, 0f)).isFalse() + } +} From 4ad47d7f967159b26c2ae2f5c53390f06b142945 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 15:12:41 +0200 Subject: [PATCH 038/146] :core:ui more blockers --- .../preference/AdaptiveIntentPreference.kt | 1 - .../preference/AdaptiveSwitchPreference.kt | 1 - .../ui/compose/preference/ListPreference.kt | 1 - .../preference/PreferenceScreenContent.kt | 23 ++++++++++++------- .../compose/preference/TextFieldPreference.kt | 1 - 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index 5589d61d1c15..2ac324051208 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt index fa7e21909ebe..87e83fc4f393 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt index 65d0a0bb1f64..6cd4830b6630 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt @@ -17,7 +17,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.background diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt index 32bb317032ec..af4851776e44 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt @@ -1,9 +1,9 @@ package app.aaps.core.ui.compose.preference -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateMap @@ -77,17 +77,24 @@ class PreferenceSectionState( companion object { - val Saver: Saver = Saver( + /** + * Saved as a flat `[key, value, key, value, ...]` list rather than a `Bundle`. + * + * A `Bundle` is Android only, and it bought nothing here: the state is a map of section key + * to expanded flag, and `listSaver` stores `String` and `Boolean` through the same + * SaveableStateRegistry that a Bundle-backed saver used. Same behaviour across + * configuration changes, no Android type in the signature. + */ + val Saver: Saver = listSaver( save = { state -> - Bundle().apply { - state.expandedSections.forEach { (k, v) -> putBoolean(k, v) } - } + state.expandedSections.flatMap { (key, expanded) -> listOf(key, expanded) } }, - restore = { bundle -> + restore = { saved -> PreferenceSectionState( expandedSections = mutableStateMapOf().apply { - bundle.keySet().forEach { key -> - put(key, bundle.getBoolean(key)) + saved.chunked(2).forEach { pair -> + val key = pair.getOrNull(0) as? String ?: return@forEach + put(key, pair.getOrNull(1) as? Boolean ?: false) } } ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt index 433887790ec3..dbf3c2b83eac 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt @@ -17,7 +17,6 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Box From 4a34333f15bf44432d4f1a008dc4c72164685587 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 20:28:23 +0200 Subject: [PATCH 039/146] :core:nssdk restore examples.json formatting --- .../aaps/core/nssdk/remotemodel/examples.json | 775 +++++++++--------- 1 file changed, 388 insertions(+), 387 deletions(-) diff --git a/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json index 0ffff23e4df7..87b19cad8b79 100644 --- a/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json +++ b/core/nssdk/src/commonMain/kotlin/app/aaps/core/nssdk/remotemodel/examples.json @@ -19,129 +19,129 @@ // G6 AAPS { -"_id": "60bace9f51e8150004f0973a", -"device": "AndroidAPS-DexcomG6", -"date": 1622855221000, -"dateString": "2021-06-05T01:07:01.000Z", -"isValid": true, -"sgv": 76, -"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp -"type": "sgv", -"created_at": "2021-06-05T01:08:47.234Z" + "_id": "60bace9f51e8150004f0973a", + "device": "AndroidAPS-DexcomG6", + "date": 1622855221000, + "dateString": "2021-06-05T01:07:01.000Z", + "isValid": true, + "sgv": 76, + "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp + "type": "sgv", + "created_at": "2021-06-05T01:08:47.234Z" }, // G6 DEXCOM APP Share { -"_id": "60cd4e7d5bcdeb30e43a248d", -"sgv": 90, -"date": 1624067551000, -"dateString": "2021-06-19T01:52:31.000Z", -"trend": 4, // 7 , 6 , 5 , 4 , 3 , 2 , 1 -"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp -"device": "share2", -"type": "sgv", -"utcOffset": 0, -"sysTime": "2021-06-19T01:52:31.000Z" + "_id": "60cd4e7d5bcdeb30e43a248d", + "sgv": 90, + "date": 1624067551000, + "dateString": "2021-06-19T01:52:31.000Z", + "trend": 4, // 7 , 6 , 5 , 4 , 3 , 2 , 1 + "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp + "device": "share2", + "type": "sgv", + "utcOffset": 0, + "sysTime": "2021-06-19T01:52:31.000Z" }, // FSL1 xDrip { -"device": "AndroidAPS", -"date": 1588557121000, -"dateString": "2020-05-04T01:52:01Z", -"sgv": 76, -"direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp -"type": "sgv", -"systime": "2020-05-04T01:52:01Z", -"utcOffset": 120 + "device": "AndroidAPS", + "date": 1588557121000, + "dateString": "2020-05-04T01:52:01Z", + "sgv": 76, + "direction": "Flat", // DoubleDown, SingleDown, FortyFiveDown, Flat, FortyFiveUp, SingleUp, DoubleUp + "type": "sgv", + "systime": "2020-05-04T01:52:01Z", + "utcOffset": 120 }, // LimiTTer xDrip { -"_id": "5ed06c9a0ea4dcb70fac6cc7", -"device": "xDrip-LimiTTer", -"date": 1590717591357, -"dateString": "2020-05-29T01:59:51.357Z", -"sgv": 114, -"delta": -2.942, -"direction": "Flat", -"type": "sgv", -"filtered": 127411.75515, -"unfiltered": 127411.75515, -"rssi": 100, -"noise": 1, -"sysTime": "2020-05-29T01:59:51.357Z", -"utcOffset": 120 + "_id": "5ed06c9a0ea4dcb70fac6cc7", + "device": "xDrip-LimiTTer", + "date": 1590717591357, + "dateString": "2020-05-29T01:59:51.357Z", + "sgv": 114, + "delta": -2.942, + "direction": "Flat", + "type": "sgv", + "filtered": 127411.75515, + "unfiltered": 127411.75515, + "rssi": 100, + "noise": 1, + "sysTime": "2020-05-29T01:59:51.357Z", + "utcOffset": 120 }, // API v3 requests for treatments { -"eventType": "BG Check", -"created_at": 1616966443000, -"units": "mg/dl", -"glucose": 57, -"NSCLIENT_ID": "1616966443000", -"identifier": "6060f32e9b9c5900045c858b", -"srvModified": 1616966443000, -"srvCreated": 1616966443000 + "eventType": "BG Check", + "created_at": 1616966443000, + "units": "mg/dl", + "glucose": 57, + "NSCLIENT_ID": "1616966443000", + "identifier": "6060f32e9b9c5900045c858b", + "srvModified": 1616966443000, + "srvCreated": 1616966443000 }, { -"eventType": "BG Check", -"created_at": 1617365936000, -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"notes": "Coucou", -"glucose": 94, -"glucoseType": "Finger", -"identifier": "606727c058f71500041e1ed3", -"srvModified": 1617365936000, -"srvCreated": 1617365936000 + "eventType": "BG Check", + "created_at": 1617365936000, + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "notes": "Coucou", + "glucose": 94, + "glucoseType": "Finger", + "identifier": "606727c058f71500041e1ed3", + "srvModified": 1617365936000, + "srvCreated": 1617365936000 }, { -"eventType": "Meal Bolus", -"carbs": 45, -"created_at": "2021-07-13T18:19:43.325Z", -"isValid": true, -"date": 1626200383325, -"identifier": "60ede2a4c574da0004a3869c", -"srvModified": 1626200383325, -"srvCreated": 1626200383325 + "eventType": "Meal Bolus", + "carbs": 45, + "created_at": "2021-07-13T18:19:43.325Z", + "isValid": true, + "date": 1626200383325, + "identifier": "60ede2a4c574da0004a3869c", + "srvModified": 1626200383325, + "srvCreated": 1626200383325 }, { -"eventType": "Meal Bolus", -"insulin": 8.1, -"created_at": "2021-07-13T11:25:12.664Z", -"date": 1626175512664, -"type": "NORMAL", -"isValid": true, -"isSMB": false, -"pumpId": 4102, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33013206", -"identifier": "60ed782dc574da0004a38595", -"srvModified": 1626175512664, -"srvCreated": 1626175512664 + "eventType": "Meal Bolus", + "insulin": 8.1, + "created_at": "2021-07-13T11:25:12.664Z", + "date": 1626175512664, + "type": "NORMAL", + "isValid": true, + "isSMB": false, + "pumpId": 4102, + "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", + "pumpSerial": "33013206", + "identifier": "60ed782dc574da0004a38595", + "srvModified": 1626175512664, + "srvCreated": 1626175512664 }, { -"eventType": "Correction Bolus", -"insulin": 0.25, -"created_at": "2021-07-13T20:44:14.441Z", -"date": 1626209054441, -"type": "SMB", -"isValid": true, -"isSMB": true, -"pumpId": 4148, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33013206", -"identifier": "60edfb34c574da0004a386d4", -"srvModified": 1626209054441, -"srvCreated": 1626209054441 + "eventType": "Correction Bolus", + "insulin": 0.25, + "created_at": "2021-07-13T20:44:14.441Z", + "date": 1626209054441, + "type": "SMB", + "isValid": true, + "isSMB": true, + "pumpId": 4148, + "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", + "pumpSerial": "33013206", + "identifier": "60edfb34c574da0004a386d4", + "srvModified": 1626209054441, + "srvCreated": 1626209054441 },{ -"eventType": "Carb Correction", -"carbs": 5, -"created_at": "2021-06-17T09:00:34.000Z", -"isValid": true, -"date": 1623920434000, -"identifier": "60cb2f351a94d4000483b692", -"srvModified": 1623920434000, -"srvCreated": 1623920434000 + "eventType": "Carb Correction", + "carbs": 5, + "created_at": "2021-06-17T09:00:34.000Z", + "isValid": true, + "date": 1623920434000, + "identifier": "60cb2f351a94d4000483b692", + "srvModified": 1623920434000, + "srvCreated": 1623920434000 }, { "created_at": "2021-05-28T19:46:43.851Z", @@ -162,328 +162,329 @@ "srvCreated": 1622231203851 }, { -"eventType": "Announcement", -"created_at": 1617350431592, -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"notes": "5g de glucides requis dans 40 min.", -"isAnnouncement": true, -"identifier": "6066cf2508a6ed0004b4ed44", -"srvModified": 1617350431592, -"srvCreated": 1617350431592 + "eventType": "Announcement", + "created_at": 1617350431592, + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "notes": "5g de glucides requis dans 40 min.", + "isAnnouncement": true, + "identifier": "6066cf2508a6ed0004b4ed44", + "srvModified": 1617350431592, + "srvCreated": 1617350431592 }, { -"eventType": "Note", -"created_at": 1617023462485, -"units": "mg/dl", -"notes": "AndroidAPS started - Logicom Le Hola FR", -"identifier": "6061d20b17619800047216b2", -"srvModified": 1617023462485, -"srvCreated": 1617023462485 + "eventType": "Note", + "created_at": 1617023462485, + "units": "mg/dl", + "notes": "AndroidAPS started - Logicom Le Hola FR", + "identifier": "6061d20b17619800047216b2", + "srvModified": 1617023462485, + "srvCreated": 1617023462485 }, { -"eventType": "Exercise", -"created_at": 1617373066000, -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"duration": 20, -"notes": "ten tab", -"identifier": "606727a658f71500041e1ed2", -"srvModified": 1617373066000, -"srvCreated": 1617373066000 + "eventType": "Exercise", + "created_at": 1617373066000, + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "duration": 20, + "notes": "ten tab", + "identifier": "606727a658f71500041e1ed2", + "srvModified": 1617373066000, + "srvCreated": 1617373066000 }, { -"eventType": "Exercise", -"isValid": true, -"created_at": "2021-07-09T18:15:22.000Z", -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"duration": 105, -"notes": "🏓", -"identifier": "60e8b223b98ea2000472cbb3", -"srvModified": 1625854522000, -"srvCreated": 1625854522000 + "eventType": "Exercise", + "isValid": true, + "created_at": "2021-07-09T18:15:22.000Z", + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "duration": 105, + "notes": "🏓", + "identifier": "60e8b223b98ea2000472cbb3", + "srvModified": 1625854522000, + "srvCreated": 1625854522000 }, { -"eventType": "Site Change", -"created_at": 1616312250000, -"units": "mg/dl", -"notes": "", -"NSCLIENT_ID": "1616312250000", -"identifier": "6056f7c1bc2dc60004e75499", -"srvModified": 1616312250000, -"srvCreated": 1616312250000 + "eventType": "Site Change", + "created_at": 1616312250000, + "units": "mg/dl", + "notes": "", + "NSCLIENT_ID": "1616312250000", + "identifier": "6056f7c1bc2dc60004e75499", + "srvModified": 1616312250000, + "srvCreated": 1616312250000 }, { -"eventType": "Sensor Change", -"created_at": 1617373059000, -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"identifier": "6067278d58f71500041e1ed1", -"srvModified": 1617373059000, -"srvCreated": 1617373059000 + "eventType": "Sensor Change", + "created_at": 1617373059000, + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "identifier": "6067278d58f71500041e1ed1", + "srvModified": 1617373059000, + "srvCreated": 1617373059000 }, { -"enteredBy": "AndroidAPS-DexcomG6", -"created_at": 1617799461000, -"eventType": "Sensor Change", -"NSCLIENT_ID": "1617961262771", -"identifier": "60702190403172000451e5dc", -"srvModified": 1617799461000, -"srvCreated": 1617799461000 + "enteredBy": "AndroidAPS-DexcomG6", + "created_at": 1617799461000, + "eventType": "Sensor Change", + "NSCLIENT_ID": "1617961262771", + "identifier": "60702190403172000451e5dc", + "srvModified": 1617799461000, + "srvCreated": 1617799461000 }, { -"eventType": "Pump Battery Change", -"created_at": 1616517575000, -"enteredBy": "AndroidAPS", -"units": "mg/dl", -"notes": "à peu près...", -"NSCLIENT_ID": "1616517575000", -"identifier": "605cbce3f9ed3b0004694ee8", -"srvModified": 1616517575000, -"srvCreated": 1616517575000 + "eventType": "Pump Battery Change", + "created_at": 1616517575000, + "enteredBy": "AndroidAPS", + "units": "mg/dl", + "notes": "à peu près...", + "NSCLIENT_ID": "1616517575000", + "identifier": "605cbce3f9ed3b0004694ee8", + "srvModified": 1616517575000, + "srvCreated": 1616517575000 }, { -"created_at": 1617576811000, -"eventType": "Pump Battery Change", -"NSCLIENT_ID": "1617577097394", -"glucoseType": "Manual", -"isValid": true, -"units": "mg/dl", -"identifier": "606a448d7c31f00004bb47ac", -"srvModified": 1617576811000, -"srvCreated": 1617576811000 + "created_at": 1617576811000, + "eventType": "Pump Battery Change", + "NSCLIENT_ID": "1617577097394", + "glucoseType": "Manual", + "isValid": true, + "units": "mg/dl", + "identifier": "606a448d7c31f00004bb47ac", + "srvModified": 1617576811000, + "srvCreated": 1617576811000 }, { -"eventType": "Insulin Change", -"created_at": 1616342559000, -"units": "mg/dl", -"notes": "Ajout manuel pour UE", -"NSCLIENT_ID": "1616342559000", -"identifier": "60576e6b5a34f900043e25f6", -"srvModified": 1616342559000, -"srvCreated": 1616342559000 + "eventType": "Insulin Change", + "created_at": 1616342559000, + "units": "mg/dl", + "notes": "Ajout manuel pour UE", + "NSCLIENT_ID": "1616342559000", + "identifier": "60576e6b5a34f900043e25f6", + "srvModified": 1616342559000, + "srvCreated": 1616342559000 }, { -"created_at": "2021-07-13T20:44:12.891Z", -"enteredBy": "openaps://AndroidAPS", -"eventType": "Temp Basal", -"isValid": true, -"duration": 60, -"rate": 0, -"type": "NORMAL", -"absolute": 0, -"pumpId": 284835, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33013206", -"identifier": "60edfb34c574da0004a386d3", -"srvModified": 1626209052891, -"srvCreated": 1626209052891 + "created_at": "2021-07-13T20:44:12.891Z", + "enteredBy": "openaps://AndroidAPS", + "eventType": "Temp Basal", + "isValid": true, + "duration": 60, + "rate": 0, + "type": "NORMAL", + "absolute": 0, + "pumpId": 284835, + "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", + "pumpSerial": "33013206", + "identifier": "60edfb34c574da0004a386d3", + "srvModified": 1626209052891, + "srvCreated": 1626209052891 }, { -"created_at": "2021-07-13T20:40:29.896Z", -"enteredBy": "openaps://AndroidAPS", -"eventType": "Temp Basal", -"isValid": true, -"duration": 3, -"rate": 2.4391549295774646, -"type": "FAKE_EXTENDED", -"absolute": 2.4391549295774646, -"pumpId": 4147, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33013206", -"extendedEmulated": { -"created_at": "2021-07-13T20:40:29.896Z", -"enteredBy": "openaps://AndroidAPS", -"eventType": "Combo Bolus", -"duration": 3, -"splitNow": 0, -"splitExt": 100, -"enteredinsulin": 0.11, -"relative": 1.8591549295774648, -"isValid": true, -"isEmulatingTempBasal": true, -"pumpId": 4147, -"pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", -"pumpSerial": "33013206" -}, -"identifier": "60edfa51c574da0004a386d0", -"srvModified": 1626208829896, -"srvCreated": 1626208829896 + "created_at": "2021-07-13T20:40:29.896Z", + "enteredBy": "openaps://AndroidAPS", + "eventType": "Temp Basal", + "isValid": true, + "duration": 3, + "rate": 2.4391549295774646, + "type": "FAKE_EXTENDED", + "absolute": 2.4391549295774646, + "pumpId": 4147, + "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", + "pumpSerial": "33013206", + "extendedEmulated": { + "created_at": "2021-07-13T20:40:29.896Z", + "enteredBy": "openaps://AndroidAPS", + "eventType": "Combo Bolus", + "duration": 3, + "splitNow": 0, + "splitExt": 100, + "enteredinsulin": 0.11, + "relative": 1.8591549295774648, + "isValid": true, + "isEmulatingTempBasal": true, + "pumpId": 4147, + "pumpType": "ACCU_CHEK_INSIGHT_BLUETOOTH", + "pumpSerial": "33013206" + }, + "identifier": "60edfa51c574da0004a386d0", + "srvModified": 1626208829896, + "srvCreated": 1626208829896 }, { -"eventType": "OpenAPS Offline", -"created_at": 1616391934628, -"enteredBy": "openaps://AndroidAPS", -"units": "mg/dl", -"duration": 15, -"NSCLIENT_ID": "1616391934628", -"identifier": "60582f005a34f900043e2845", -"srvModified": 1616391934628, -"srvCreated": 1616391934628 + "eventType": "OpenAPS Offline", + "created_at": 1616391934628, + "enteredBy": "openaps://AndroidAPS", + "units": "mg/dl", + "duration": 15, + "NSCLIENT_ID": "1616391934628", + "identifier": "60582f005a34f900043e2845", + "srvModified": 1616391934628, + "srvCreated": 1616391934628 }, { -"created_at": "2021-06-26T13:36:47.000Z", -"enteredBy": "openaps://AndroidAPS", -"isValid": true, -"eventType": "Profile Switch", -"duration": 0, -"profile": "Tuned 13/01 90%Lyum", -"profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":45},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":45},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":46},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":49},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":52},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":55},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":54},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":51},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":49},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":49}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":4.3},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":5.5},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":5}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.7},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.78},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.85},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.75},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.61},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.69},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.64},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.68},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.65},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.64},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.65},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.67},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.74},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.76},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.76},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.77},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.68},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.72},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.65},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.7},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.62},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.62},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.58},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.55}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", -"timeshift": 0, -"percentage": 100, -"identifier": "60d72d80aec46a0004f95163", -"srvModified": 1624714607000, -"srvCreated": 1624714607000 + "created_at": "2021-06-26T13:36:47.000Z", + "enteredBy": "openaps://AndroidAPS", + "isValid": true, + "eventType": "Profile Switch", + "duration": 0, + "profile": "Tuned 13/01 90%Lyum", + "profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":45},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":45},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":46},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":49},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":52},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":55},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":54},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":51},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":49},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":49}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":4.3},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":5.5},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":5}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.7},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.78},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.85},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.75},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.61},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.69},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.64},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.68},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.65},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.64},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.65},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.67},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.74},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.76},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.76},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.77},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.68},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.72},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.65},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.7},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.62},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.62},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.58},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.55}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", + "timeshift": 0, + "percentage": 100, + "identifier": "60d72d80aec46a0004f95163", + "srvModified": 1624714607000, + "srvCreated": 1624714607000 }, { -"created_at": "2021-06-13T07:20:33.000Z", -"enteredBy": "openaps://AndroidAPS", -"isValid": true, -"eventType": "Profile Switch", -"duration": 150, -"profile": "Tuned 13/01 90%Lyum(75%)", -"profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":60},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":60},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":61.33333333333333},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":65.33333333333333},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":69.33333333333333},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":73.33333333333333},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":72},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":68},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":65.33333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":65.33333333333333}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":5.7333333333333325},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":7.333333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":6.666666666666666}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.5249999999999999},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.585},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.6375},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.5625},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.4575},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.5175},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.48},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.51},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.48750000000000004},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.48},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.48750000000000004},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.5025000000000001},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.5549999999999999},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.5700000000000001},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.5700000000000001},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.5775},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.51},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.54},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.48750000000000004},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.5249999999999999},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.46499999999999997},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.46499999999999997},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.43499999999999994},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.41250000000000003}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", -"timeshift": 0, -"percentage": 100, -"identifier": "60c5b1f41b3715000420af27", -"srvModified": 1623568833000, -"srvCreated": 1623568833000 + "created_at": "2021-06-13T07:20:33.000Z", + "enteredBy": "openaps://AndroidAPS", + "isValid": true, + "eventType": "Profile Switch", + "duration": 150, + "profile": "Tuned 13/01 90%Lyum(75%)", + "profileJson": "{\"units\":\"mg\\/dl\",\"dia\":5,\"timezone\":\"Africa\\/Cairo\",\"sens\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":60},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":60},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":61.33333333333333},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":65.33333333333333},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":69.33333333333333},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":73.33333333333333},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":72},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":68},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":65.33333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":65.33333333333333}],\"carbratio\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":5.7333333333333325},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":7.333333333333333},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":6.666666666666666}],\"basal\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":0.5249999999999999},{\"time\":\"01:00\",\"timeAsSeconds\":3600,\"value\":0.585},{\"time\":\"02:00\",\"timeAsSeconds\":7200,\"value\":0.6375},{\"time\":\"03:00\",\"timeAsSeconds\":10800,\"value\":0.5625},{\"time\":\"04:00\",\"timeAsSeconds\":14400,\"value\":0.4575},{\"time\":\"05:00\",\"timeAsSeconds\":18000,\"value\":0.5175},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":0.48},{\"time\":\"07:00\",\"timeAsSeconds\":25200,\"value\":0.51},{\"time\":\"08:00\",\"timeAsSeconds\":28800,\"value\":0.48750000000000004},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":0.48},{\"time\":\"10:00\",\"timeAsSeconds\":36000,\"value\":0.48750000000000004},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":0.5025000000000001},{\"time\":\"12:00\",\"timeAsSeconds\":43200,\"value\":0.5549999999999999},{\"time\":\"13:00\",\"timeAsSeconds\":46800,\"value\":0.5700000000000001},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":0.5700000000000001},{\"time\":\"15:00\",\"timeAsSeconds\":54000,\"value\":0.5775},{\"time\":\"16:00\",\"timeAsSeconds\":57600,\"value\":0.51},{\"time\":\"17:00\",\"timeAsSeconds\":61200,\"value\":0.54},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":0.48750000000000004},{\"time\":\"19:00\",\"timeAsSeconds\":68400,\"value\":0.5249999999999999},{\"time\":\"20:00\",\"timeAsSeconds\":72000,\"value\":0.46499999999999997},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":0.46499999999999997},{\"time\":\"22:00\",\"timeAsSeconds\":79200,\"value\":0.43499999999999994},{\"time\":\"23:00\",\"timeAsSeconds\":82800,\"value\":0.41250000000000003}],\"target_low\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}],\"target_high\":[{\"time\":\"00:00\",\"timeAsSeconds\":0,\"value\":100},{\"time\":\"06:00\",\"timeAsSeconds\":21600,\"value\":90},{\"time\":\"09:00\",\"timeAsSeconds\":32400,\"value\":100},{\"time\":\"11:00\",\"timeAsSeconds\":39600,\"value\":90},{\"time\":\"14:00\",\"timeAsSeconds\":50400,\"value\":100},{\"time\":\"18:00\",\"timeAsSeconds\":64800,\"value\":90},{\"time\":\"21:00\",\"timeAsSeconds\":75600,\"value\":100}]}", + "timeshift": 0, + "percentage": 100, + "identifier": "60c5b1f41b3715000420af27", + "srvModified": 1623568833000, + "srvCreated": 1623568833000 }, { -"eventType": "Temporary Target", -"duration": 60, -"isValid": true, -"created_at": "2021-07-10T05:04:11.566Z", -"enteredBy": "AndroidAPS", -"reason": "Automation", -"targetBottom": 110, -"targetTop": 110, -"units": "mg/dl", -"identifier": "60e92a644fc2eb00045ece1b", -"srvModified": 1625893451566, -"srvCreated": 1625893451566 + "eventType": "Temporary Target", + "duration": 60, + "isValid": true, + "created_at": "2021-07-10T05:04:11.566Z", + "enteredBy": "AndroidAPS", + "reason": "Automation", + "targetBottom": 110, + "targetTop": 110, + "units": "mg/dl", + "identifier": "60e92a644fc2eb00045ece1b", + "srvModified": 1625893451566, + "srvCreated": 1625893451566 }, { -"eventType": "Temporary Target", -"duration": 120, -"isValid": true, -"created_at": "2021-07-09T20:30:21.627Z", -"enteredBy": "AndroidAPS", -"reason": "Hypo", -"targetBottom": 140, -"targetTop": 140, -"units": "mg/dl", -"identifier": "60e8b1f2b98ea2000472cbb1", -"srvModified": 1625862621627, -"srvCreated": 1625862621627 + "eventType": "Temporary Target", + "duration": 120, + "isValid": true, + "created_at": "2021-07-09T20:30:21.627Z", + "enteredBy": "AndroidAPS", + "reason": "Hypo", + "targetBottom": 140, + "targetTop": 140, + "units": "mg/dl", + "identifier": "60e8b1f2b98ea2000472cbb1", + "srvModified": 1625862621627, + "srvCreated": 1625862621627 }, { -"eventType": "Bolus Wizard", -"created_at": "2021-07-13T18:59:43.325Z", -"isValid": true, -"bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", -"date": 1626202783325, -"glucose": 134, -"units": "mg/dl", -"notes": "", -"identifier": "60ede2a4c574da0004a3869d", -"srvModified": 1626202783325, -"srvCreated": 1626202783325 + "eventType": "Bolus Wizard", + "created_at": "2021-07-13T18:59:43.325Z", + "isValid": true, + "bolusCalculatorResult": "{\"basalIOB\":-0.247,\"bolusIOB\":-1.837,\"carbs\":45.0,\"carbsInsulin\":9.0,\"cob\":0.0,\"cobInsulin\":0.0,\"dateCreated\":1626202788810,\"glucoseDifference\":44.0,\"glucoseInsulin\":0.8979591836734694,\"glucoseTrend\":5.5,\"glucoseValue\":134.0,\"ic\":5.0,\"id\":331,\"interfaceIDs_backing\":{\"nightscoutId\":\"60ede2a4c574da0004a3869d\"},\"isValid\":true,\"isf\":49.0,\"note\":\"\",\"otherCorrection\":0.0,\"percentageCorrection\":90,\"profileName\":\"Tuned 13/01 90%Lyum\",\"superbolusInsulin\":0.0,\"targetBGHigh\":90.0,\"targetBGLow\":90.0,\"timestamp\":1626202783325,\"totalInsulin\":7.34,\"trendInsulin\":0.336734693877551,\"utcOffset\":7200000,\"version\":1,\"wasBasalIOBUsed\":true,\"wasBolusIOBUsed\":true,\"wasCOBUsed\":true,\"wasGlucoseUsed\":true,\"wasSuperbolusUsed\":false,\"wasTempTargetUsed\":false,\"wasTrendUsed\":true,\"wereCarbsUsed\":false}", + "date": 1626202783325, + "glucose": 134, + "units": "mg/dl", + "notes": "", + "identifier": "60ede2a4c574da0004a3869d", + "srvModified": 1626202783325, + "srvCreated": 1626202783325 }, + DEVICE STATUS with configuration --------------------------------- { -"_id": "635abf2069a34517e83768cd", -"created_at": "2022-10-27T17:25:49.730Z", -"device": "openaps://samsung SM-G970F", -"pump": { -"battery": { -"percent": 100 -}, -"status": { -"status": "normal", -"timestamp": "2022-10-27T17:16:11.504Z" -}, -"extended": { -"Version": "3.1.0.3-dev-c-nscv3-8da78d7351-2022.10.25-19:56", -"LastBolus": "10/27/22 18:40", -"LastBolusAmount": 0.35, -"TempBasalAbsoluteRate": 0, -"TempBasalStart": "10/27/22 18:50", -"TempBasalRemaining": 24, -"BaseBasalRate": 1, -"ActiveProfile": "LocalProfile1" -}, -"reservoir": 191, -"clock": "2022-10-27T17:25:49.759Z" -}, -"openaps": { -"suggested": { -"temp": "absolute", -"bg": 72, -"tick": -6, -"eventualBG": 4, -"snoozeBG": 4, -"predBGs": { -"IOB": [72, 61, 51, 42, 39, 39, 39, 39, 39, 39, 39, 39, 39] -}, -"COB": 0, -"IOB": 0.052, -"reason": "COB: 0, Dev: -66, BGI: -0.88, ISF: 2.0, Target: 6.0; BG 4.0<4.4, but 25m left and 0 ~ req 0U/hr: no action required", -"timestamp": "2022-10-27T17:25:49.726Z" -}, -"iob": { -"iob": 0.052, -"basaliob": 0.052, -"activity": 0.0049, -"time": "2022-10-27T17:25:49.726Z" -} -}, -"uploaderBattery": 100, -"configuration": { -"insulin": 5, -"insulinConfiguration": {}, -"sensitivity": 2, -"sensitivityConfiguration": { -"openapsama_min_5m_carbimpact": 10, -"absorption_cutoff": 4, -"autosens_max": 1.2, -"autosens_min": 0.7 -}, -"overviewConfiguration": { -"units": "mmol", -"eatingsoon_duration": 0, -"eatingsoon_target": 0, -"activity_duration": 0, -"activity_target": 0, -"hypo_duration": 0, -"hypo_target": 0, -"low_mark": 4, -"high_mark": 0, -"statuslights_cage_warning": 48, -"statuslights_cage_critical": 72, -"statuslights_iage_warning": 72, -"statuslights_iage_critical": 144, -"statuslights_sage_warning": 216, -"statuslights_sage_critical": 240, -"statuslights_sbat_warning": 25, -"statuslights_sbat_critical": 5, -"statuslights_bage_warning": 216, -"statuslights_bage_critical": 240, -"statuslights_res_warning": 80, -"statuslights_res_critical": 10, -"statuslights_bat_warning": 25, -"statuslights_bat_critical": 5, -"boluswizard_percentage": 60 -}, -"safetyConfiguration": { -"age": "teenage", -"treatmentssafety_maxbolus": 4, -"treatmentssafety_maxcarbs": 60 -}, -"pump": "DanaR", -"version": "3.1.0.3-dev-c-nscv3" -}, -"mills": 1666891549730 + "_id": "635abf2069a34517e83768cd", + "created_at": "2022-10-27T17:25:49.730Z", + "device": "openaps://samsung SM-G970F", + "pump": { + "battery": { + "percent": 100 + }, + "status": { + "status": "normal", + "timestamp": "2022-10-27T17:16:11.504Z" + }, + "extended": { + "Version": "3.1.0.3-dev-c-nscv3-8da78d7351-2022.10.25-19:56", + "LastBolus": "10/27/22 18:40", + "LastBolusAmount": 0.35, + "TempBasalAbsoluteRate": 0, + "TempBasalStart": "10/27/22 18:50", + "TempBasalRemaining": 24, + "BaseBasalRate": 1, + "ActiveProfile": "LocalProfile1" + }, + "reservoir": 191, + "clock": "2022-10-27T17:25:49.759Z" + }, + "openaps": { + "suggested": { + "temp": "absolute", + "bg": 72, + "tick": -6, + "eventualBG": 4, + "snoozeBG": 4, + "predBGs": { + "IOB": [72, 61, 51, 42, 39, 39, 39, 39, 39, 39, 39, 39, 39] + }, + "COB": 0, + "IOB": 0.052, + "reason": "COB: 0, Dev: -66, BGI: -0.88, ISF: 2.0, Target: 6.0; BG 4.0<4.4, but 25m left and 0 ~ req 0U/hr: no action required", + "timestamp": "2022-10-27T17:25:49.726Z" + }, + "iob": { + "iob": 0.052, + "basaliob": 0.052, + "activity": 0.0049, + "time": "2022-10-27T17:25:49.726Z" + } + }, + "uploaderBattery": 100, + "configuration": { + "insulin": 5, + "insulinConfiguration": {}, + "sensitivity": 2, + "sensitivityConfiguration": { + "openapsama_min_5m_carbimpact": 10, + "absorption_cutoff": 4, + "autosens_max": 1.2, + "autosens_min": 0.7 + }, + "overviewConfiguration": { + "units": "mmol", + "eatingsoon_duration": 0, + "eatingsoon_target": 0, + "activity_duration": 0, + "activity_target": 0, + "hypo_duration": 0, + "hypo_target": 0, + "low_mark": 4, + "high_mark": 0, + "statuslights_cage_warning": 48, + "statuslights_cage_critical": 72, + "statuslights_iage_warning": 72, + "statuslights_iage_critical": 144, + "statuslights_sage_warning": 216, + "statuslights_sage_critical": 240, + "statuslights_sbat_warning": 25, + "statuslights_sbat_critical": 5, + "statuslights_bage_warning": 216, + "statuslights_bage_critical": 240, + "statuslights_res_warning": 80, + "statuslights_res_critical": 10, + "statuslights_bat_warning": 25, + "statuslights_bat_critical": 5, + "boluswizard_percentage": 60 + }, + "safetyConfiguration": { + "age": "teenage", + "treatmentssafety_maxbolus": 4, + "treatmentssafety_maxcarbs": 60 + }, + "pump": "DanaR", + "version": "3.1.0.3-dev-c-nscv3" + }, + "mills": 1666891549730 } \ No newline at end of file From f184269d6d6e08ab1253c8a824cebce5695a400d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 20:36:24 +0200 Subject: [PATCH 040/146] Remove CMP spike --- _docs/KMP_IOS_FEASIBILITY.md | 46 +++++ settings.gradle | 2 - spike/cmp/build.gradle.kts | 59 ------ .../aaps/spike/cmp/TextRefResource.android.kt | 19 -- .../kotlin/app/aaps/spike/cmp/Helpers.kt | 95 ---------- .../app/aaps/spike/cmp/PlusMinusEdit.kt | 179 ------------------ .../app/aaps/spike/cmp/TextRefResource.kt | 19 -- .../app/aaps/spike/cmp/TextRefResource.ios.kt | 22 --- 8 files changed, 46 insertions(+), 395 deletions(-) delete mode 100644 spike/cmp/build.gradle.kts delete mode 100644 spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt delete mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt delete mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt delete mode 100644 spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt delete mode 100644 spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index fed0aee45801..818b6afc8dbc 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -1606,6 +1606,52 @@ formatter cache, above): guard keeps the old contract on a public interface method. +### Wave 17 - what the in-repo CMP spike proved, and its recipe (spike now deleted) + +Wave 13's spike was a standalone project outside the repo and only asked about **compose-resources**. +A second spike, `:spike:cmp`, lived *inside* the build for several waves and asked a different +question: does Compose Multiplatform work in **this** build, next to the androidx Compose the app +already uses. It has been deleted now that it has nothing left to answer, so its result and its build +recipe are recorded here. + +**What it proved.** Two real `:core:ui` files - `PlusMinusEdit` and its `Helpers` - compiled unchanged +from `commonMain` for `android`, `iosArm64` and `iosSimulatorArm64`, against the real +`:core:data` and `:core:keys` (not stubs), with `stringResource(TextRef)` as an `expect`/`actual` +seam. That is the whole `:core:ui` module flip in miniature, so the flip is a build-configuration +problem rather than an open technical question. + +**The recipe, which the flip should reuse:** + +```kotlin +plugins { + kotlin("multiplatform") + alias(libs.plugins.android.kmp.library) // NOT com.android.library - AGP 9 refuses that with KMP + alias(libs.plugins.compose.compiler) // ships with Kotlin; org.jetbrains.compose hard-fails without it + alias(libs.plugins.compose.multiplatform) +} +``` + +Four deliberate omissions, each of which would have cost a debugging session to rediscover: + +- **No `compose.components.resources`.** compose-resources was rejected for AAPS (it cannot match a + region-less locale against a region-pinned folder). `generateResClass` defaults to `auto`, which + generates nothing unless that dependency is present - so leaving it out *is* the opt-out. +- **No `jvm()` target.** It pulls in the desktop Compose surface (skiko-awt) and gives the module + another way to fail without saying anything about iOS. +- **No `androidResources`.** Off by default for a KMP library. `:core:ui` *does* own `res/`, so unlike + the spike it must enable it - and that is exactly where CMP-9547 (resources not packaged under + AGP 9) becomes relevant, which it could not be for the spike. +- **`lint { checkReleaseBuilds = false }` restated inline**, because `android-module-dependencies` + applies `com.android.library` and so cannot be applied to a multiplatform module. Same reason as + `:core:keys`. + +`material-icons-extended` needed its CMP artifact (`libs.cmp.material.icons.extended`): both copied +files use `Icons.Filled.Remove`, which is not in the core icon set. + +The general form of the question was already answered in public - coil-kt/coil and plainhub/plain-app +both ship these four plugins on Kotlin 2.4.10 + AGP 9.3.1 + CMP 1.11.1 - so the spike only ever +checked that nothing repo-specific interferes. Nothing did. + --- ## 9. Open decisions diff --git a/settings.gradle b/settings.gradle index 976b3b8e889b..bbc57b910f2d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,8 +13,6 @@ include ':core:nssdk' include ':core:objects' include ':core:utils' include ':core:ui' -// Throwaway. Answers whether Compose Multiplatform works in THIS build; delete once decided. -include ':spike:cmp' include ':database:impl' include ':database:persistence' include ':implementation' diff --git a/spike/cmp/build.gradle.kts b/spike/cmp/build.gradle.kts deleted file mode 100644 index 030b959cd0b6..000000000000 --- a/spike/cmp/build.gradle.kts +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - kotlin("multiplatform") - // NOT com.android.library - AGP 9 refuses that together with the multiplatform plugin. - alias(libs.plugins.android.kmp.library) - // The Compose COMPILER, which ships with Kotlin and compiles @Composable for every target - // including Kotlin/Native. org.jetbrains.compose below hard-fails without it. - alias(libs.plugins.compose.compiler) - // The Compose Multiplatform framework itself. - alias(libs.plugins.compose.multiplatform) -} - -// THROWAWAY SPIKE. Not part of the app, consumed by nothing, safe to delete. -// -// It answers one question: does Compose Multiplatform work in THIS build - this Kotlin, this AGP, -// this catalog, next to the androidx Compose the app already uses. The general form of that question -// is already answered in public (coil-kt/coil and plainhub/plain-app both ship the same four plugins -// on Kotlin 2.4.10 + AGP 9.3.1 + CMP 1.11.1), so this only checks that nothing repo-specific -// interferes. -// -// Deliberately NOT here: -// - compose.components.resources. compose-resources was rejected for AAPS - it cannot match a -// region-less locale against a region-pinned folder. generateResClass defaults to `auto`, which -// generates nothing unless that dependency is present, so leaving it out is the whole opt-out. -// - a jvm() target. It would pull in the desktop Compose surface (skiko-awt) and give the spike -// another way to fail without saying anything about iOS. -// - androidResources. Off by default for a KMP library, and this module owns no res/, which is also -// why CMP-9547 (resources not packaged under AGP 9) cannot apply here. -kotlin { - android { - namespace = "app.aaps.spike.cmp" - compileSdk = Versions.compileSdk - minSdk = Versions.minSdk - compilerOptions { jvmTarget.set(Versions.jvmTarget) } - // Restated because android-module-dependencies applies com.android.library and so cannot be - // applied to a multiplatform module - same reason as core/keys. - lint { checkReleaseBuilds = false } - } - - // Apple klibs cross compile on Windows. Linking and running still need a Mac and report SKIPPED. - iosArm64() - iosSimulatorArm64() - - sourceSets { - commonMain.dependencies { - implementation(libs.cmp.runtime) - implementation(libs.cmp.foundation) - implementation(libs.cmp.ui) - implementation(libs.cmp.material3) - // Needed by the real AAPS files copied in below: both use Icons.Filled.Remove, which is - // in the extended set rather than the core one. - implementation(libs.cmp.material.icons.extended) - - // The point of the spike is UI on top of the REAL shared spine, not against stubs. - // Both of these already build for iosArm64 / iosSimulatorArm64. - implementation(project(":core:data")) - implementation(project(":core:keys")) - } - } -} diff --git a/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt b/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt deleted file mode 100644 index 1a3aca5f6fd3..000000000000 --- a/spike/cmp/src/androidMain/kotlin/app/aaps/spike/cmp/TextRefResource.android.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.aaps.spike.cmp - -import androidx.compose.runtime.Composable -import app.aaps.core.keys.KeysStringIds -import app.aaps.core.keys.interfaces.TextRef - -/** Android resolves through AAPT, exactly as `:core:ui` does today. */ -@Composable -actual fun stringResource(ref: TextRef): String = when (ref) { - is TextRef.Literal -> ref.text - is TextRef.AndroidRes -> - if (ref.args.isEmpty()) androidx.compose.ui.res.stringResource(ref.id) - else androidx.compose.ui.res.stringResource(ref.id, *ref.args.toTypedArray()) - - is TextRef.Named -> { - val id = KeysStringIds.idOf(ref.name) - if (id == null) ref.name else androidx.compose.ui.res.stringResource(id) - } -} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt deleted file mode 100644 index a083507c4c72..000000000000 --- a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/Helpers.kt +++ /dev/null @@ -1,95 +0,0 @@ -package app.aaps.spike.cmp - -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.waitForUpOrCancellation -import androidx.compose.material3.FilledTonalIconButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalHapticFeedback -import kotlinx.coroutines.delay -import kotlin.math.pow -import kotlin.math.roundToInt -import kotlin.math.roundToLong - -/** - * Copied from `:core:ui`'s SliderWithButtons.kt, with ONE change, and that change is a finding. - * - * The original reads: - * ``` - * val factor = Math.pow(10.0, decimals.toDouble()) - * return Math.round(scaled * factor) / factor - * ``` - * `Math` is `java.lang.Math` - JVM only. `java.lang` needs no import statement, so no amount of - * grepping imports finds it; the file looks clean and is not. That is the same mistake shape the - * feasibility note already records twice, and it means the count of `:core:ui` files that can move - * is optimistic wherever it was derived from imports alone. - * - * `kotlin.math` is a drop-in replacement and works on every target. - */ -internal fun roundToStep(value: Double, step: Double): Double { - val scaled = (value / step).roundToInt() * step - // Fix floating point precision errors (e.g., 6.1000000000005 -> 6.1) - val decimals = step.toString().substringAfter('.', "").length - val factor = 10.0.pow(decimals.toDouble()) - return (scaled * factor).roundToLong() / factor -} - -/** - * Copied verbatim from `:core:ui`. Kept because it exercises pointer input, haptics and a coroutine - * loop - three things a trivial spike screen would not touch. - */ -@Composable -fun RepeatingIconButton( - onClick: () -> Unit, - enabled: Boolean, - modifier: Modifier = Modifier, - initialDelayMs: Long = 500L, - maxDelayMs: Long = 200L, - minDelayMs: Long = 50L, - accelerationFactor: Float = 0.8f, - content: @Composable () -> Unit -) { - var isPressed by remember { mutableStateOf(false) } - val currentOnClick by rememberUpdatedState(onClick) - val haptic = LocalHapticFeedback.current - - LaunchedEffect(isPressed, enabled) { - if (isPressed && enabled) { - delay(initialDelayMs) - var currentDelay = maxDelayMs.toFloat() - while (isPressed && enabled) { - currentOnClick() - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) - delay(currentDelay.toLong()) - currentDelay = (currentDelay * accelerationFactor).coerceAtLeast(minDelayMs.toFloat()) - } - } - } - - FilledTonalIconButton( - onClick = { - onClick() - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) - }, - enabled = enabled, - modifier = modifier.pointerInput(Unit) { - awaitEachGesture { - awaitFirstDown(requireUnconsumed = false) - isPressed = true - waitForUpOrCancellation() - isPressed = false - } - } - ) { - content() - } -} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt deleted file mode 100644 index cf574be790e2..000000000000 --- a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/PlusMinusEdit.kt +++ /dev/null @@ -1,179 +0,0 @@ -package app.aaps.spike.cmp - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import app.aaps.core.data.format.NumberFormat -import app.aaps.core.keys.interfaces.TextRef -import kotlin.math.roundToInt - -/** - * Copied VERBATIM from `core/ui/.../compose/PlusMinusEdit.kt`, changing only the package. - * - * Chosen on purpose as the hardest realistic case rather than the easiest: `TextField` plus - * `KeyboardOptions`, `KeyboardActions`, `ImeAction`, focus handling and text selection is exactly the - * area where Compose Multiplatform on iOS is weakest, so a spike that only draws cards and icons - * would prove nothing. It also pulls in `NumberFormat` from `:core:data` and `TextRef` from - * `:core:keys` - both already multiplatform - so this compiles the UI on top of the real shared spine - * rather than against stubs. - * - * Original doc follows. - * - * Compact `[ − ] [ editable value ] [ + ]` stepper for inline numeric editing. - * - * The TextField commits its value on focus loss or IME Done — any action button on the - * same screen that depends on the value must call `focusManager.clearFocus()` first to - * flush pending text. Long-pressing +/- auto-repeats; range boundaries disable the - * corresponding button. - */ -@Composable -fun PlusMinusEdit( - value: Double, - onValueChange: (Double) -> Unit, - valueRange: ClosedFloatingPointRange, - step: Double, - valueFormat: NumberFormat = NumberFormat.DECIMAL_1, - unitLabel: TextRef? = null, - enabled: Boolean = true, - modifier: Modifier = Modifier -) { - val focusManager = LocalFocusManager.current - val minValue = valueRange.start - val maxValue = valueRange.endInclusive - - var isFocused by remember { mutableStateOf(false) } - var textFieldValue by remember { - mutableStateOf(TextFieldValue(valueFormat.format(value))) - } - var isError by remember { mutableStateOf(false) } - - // Sync external value → text when not focused (e.g., +/- buttons or external update). - LaunchedEffect(value) { - if (!isFocused) { - textFieldValue = TextFieldValue(valueFormat.format(value)) - isError = false - } - } - - val resolvedUnitLabel = unitLabel?.let { stringResource(it) } ?: "" - - fun validateAndCommit(text: String) { - val cleaned = text.trim().replace(",", ".") - val parsed = cleaned.toDoubleOrNull() - if (parsed == null) { - isError = true - } else { - isError = false - onValueChange(parsed.coerceIn(minValue, maxValue)) - } - } - - // Flush pending text when leaving composition (back press without tapping Done). - val latestTextProvider by rememberUpdatedState({ textFieldValue.text }) - val latestIsFocused by rememberUpdatedState(isFocused) - DisposableEffect(Unit) { - onDispose { - if (latestIsFocused) validateAndCommit(latestTextProvider()) - } - } - - fun stepValue(direction: Int) { - val newValue = roundToStep(value + direction * step, step).coerceIn(minValue, maxValue) - textFieldValue = TextFieldValue(valueFormat.format(newValue)) - isError = false - onValueChange(newValue) - } - - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - RepeatingIconButton( - onClick = { stepValue(-1) }, - enabled = enabled && value > minValue, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Remove, - contentDescription = "-", - modifier = Modifier.size(16.dp) - ) - } - - TextField( - value = textFieldValue, - onValueChange = { newValue -> - textFieldValue = newValue - if (isError) isError = false - }, - singleLine = true, - enabled = enabled, - isError = isError, - trailingIcon = if (resolvedUnitLabel.isNotEmpty()) { - { Text(resolvedUnitLabel) } - } else null, - keyboardOptions = KeyboardOptions( - keyboardType = if (step != step.roundToInt().toDouble()) - KeyboardType.Decimal else KeyboardType.Number, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - validateAndCommit(textFieldValue.text) - focusManager.clearFocus() - } - ), - modifier = Modifier - .weight(1f) - .onFocusChanged { focusState -> - if (isFocused && !focusState.isFocused) { - validateAndCommit(textFieldValue.text) - } - if (!isFocused && focusState.isFocused) { - textFieldValue = textFieldValue.copy( - selection = TextRange(0, textFieldValue.text.length) - ) - } - isFocused = focusState.isFocused - } - ) - - RepeatingIconButton( - onClick = { stepValue(1) }, - enabled = enabled && value < maxValue, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = "+", - modifier = Modifier.size(16.dp) - ) - } - } -} diff --git a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt b/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt deleted file mode 100644 index 96eceb7785c0..000000000000 --- a/spike/cmp/src/commonMain/kotlin/app/aaps/spike/cmp/TextRefResource.kt +++ /dev/null @@ -1,19 +0,0 @@ -package app.aaps.spike.cmp - -import androidx.compose.runtime.Composable -import app.aaps.core.keys.interfaces.TextRef - -/** - * The seam the whole `:core:ui` question turns on. - * - * 54 files in `:core:ui` call `androidx.compose.ui.res.stringResource`, which does not exist off - * Android, and `PlusMinusEdit` reaches it indirectly through the same-package `stringResource(TextRef)` - * resolver. So "can this file move to commonMain" is really "can the TextRef resolver be an - * expect/actual". This is that shape, proved rather than assumed. - * - * Android keeps AAPT exactly as the app does today. iOS has no resource system here yet - and does - * not need one for the spike, because the question is whether the SHAPE compiles for Kotlin/Native, - * not whether iOS can render Czech. - */ -@Composable -expect fun stringResource(ref: TextRef): String diff --git a/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt b/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt deleted file mode 100644 index 3908c74e5654..000000000000 --- a/spike/cmp/src/iosMain/kotlin/app/aaps/spike/cmp/TextRefResource.ios.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.aaps.spike.cmp - -import androidx.compose.runtime.Composable -import app.aaps.core.keys.interfaces.TextRef - -/** - * iOS has no resource system wired up here, and does not need one to answer the spike's question. - * - * [TextRef.Named] is the form a real iOS client would resolve, through a generated name-to-text table - * built from the same `strings.xml` - the client only needs the system language, so that table is a - * small generated file rather than a resource framework. Until it exists, showing the name is honest: - * it is visibly wrong on screen rather than silently blank. - * - * [TextRef.AndroidRes] cannot be resolved off Android by construction. It is an `Int` from AAPT, so - * a module that still hands one out has not finished moving - which is the point of the type. - */ -@Composable -actual fun stringResource(ref: TextRef): String = when (ref) { - is TextRef.Literal -> ref.text - is TextRef.Named -> ref.name - is TextRef.AndroidRes -> "?res:${ref.id}" -} From 7ba30dcdc65907044ae99258ea7d1659bb5a6bb3 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 20:53:44 +0200 Subject: [PATCH 041/146] :core:interfaces TextRef for data enums --- .../graph/profile/ProfileViewerContent.kt | 1 + core/interfaces/build.gradle.kts | 28 +++++++++ .../interfaces/insulin/ConcentrationType.kt | 31 +++++----- .../core/interfaces/insulin/InsulinType.kt | 22 +++---- .../interfaces/resources/ResourceHelper.kt | 20 +++--- .../interfaces/rx/weardata/CwfMetaDataKey.kt | 62 +++++++++---------- .../aaps/core/ui/compose/TextRefResource.kt | 13 ++-- .../insulin/InsulinImplMigrationTest.kt | 11 ++++ .../insulin/InsulinImplSyncTest.kt | 9 +++ .../sync/wear/compose/WearViewModel.kt | 4 +- .../sync/wear/compose/WearViewModelTest.kt | 4 ++ .../insulinManagement/InsulinCarouselCard.kt | 1 + .../InsulinManagementScreen.kt | 1 + 13 files changed, 133 insertions(+), 74 deletions(-) diff --git a/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileViewerContent.kt b/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileViewerContent.kt index 2ddf3f8cca27..0f8b521ed42b 100644 --- a/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileViewerContent.kt +++ b/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileViewerContent.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 445a23d42a0a..39dfbced474b 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min plugins { @@ -23,6 +24,33 @@ android { } } +// Same generator as :core:keys and :core:ui, pointed at this module's strings. It lets the data +// enums here (ConcentrationType, InsulinType, CwfMetadataKey) carry a TextRef instead of an R.string +// Int, so they stop being Android-only. The strings themselves do not move, and AAPT keeps resolving +// them on Android exactly as before. +extensions.configure("androidComponents") { + onVariants { variant -> + val taskProvider = tasks.register( + "generate${variant.name.replaceFirstChar { it.uppercase() }}InterfacesStrings", + GenerateKeyStringsTask::class.java + ) { + resDir.set(layout.projectDirectory.dir("src/main/res")) + packageName.set("app.aaps.core.interfaces") + owner.set("interfaces") + objectName.set("InterfacesStrings") + idsObjectName.set("InterfacesStringIds") + reportFile.set(layout.buildDirectory.file("reports/interfacesStrings/${variant.name}-translations.txt")) + // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the + // task name, so both properties would land on one directory and the second file written + // would delete the first. + commonOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/${variant.name}/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/${variant.name}/android")) + } + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) + variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) + } +} + dependencies { implementation(project(":core:data")) api(project(":core:keys")) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt index 1f602a8592b1..32c5a45a502f 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt @@ -1,24 +1,21 @@ package app.aaps.core.interfaces.insulin -import androidx.annotation.StringRes -import app.aaps.core.data.model.ICfg -import app.aaps.core.interfaces.R -import app.aaps.core.interfaces.insulin.InsulinType.OREF_RAPID_ACTING -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.InterfacesStrings +import app.aaps.core.keys.interfaces.TextRef -enum class ConcentrationType(val value: Double, @StringRes val label: Int) { - UNKNOWN(-1.0, R.string.unknown), - U10(0.1, R.string.u10), - U40(0.4, R.string.u40), - U50(0.5, R.string.u50), - U100(1.0, R.string.u100), - U200(2.0, R.string.u200), - U300(3.0, R.string.u300), - U500(5.0, R.string.u500); +enum class ConcentrationType(val value: Double, val label: TextRef) { + UNKNOWN(-1.0, InterfacesStrings.unknown), + U10(0.1, InterfacesStrings.u10), + U40(0.4, InterfacesStrings.u40), + U50(0.5, InterfacesStrings.u50), + U100(1.0, InterfacesStrings.u100), + U200(2.0, InterfacesStrings.u200), + U300(3.0, InterfacesStrings.u300), + U500(5.0, InterfacesStrings.u500); companion object { - fun fromDouble(type: Double) = values().firstOrNull {it.value == type} ?:UNKNOWN - fun fromInt(type: Int) = values().firstOrNull {it.value * 100 == type.toDouble()} ?:UNKNOWN + fun fromDouble(type: Double) = entries.firstOrNull { it.value == type } ?: UNKNOWN + fun fromInt(type: Int) = entries.firstOrNull { it.value * 100 == type.toDouble() } ?: UNKNOWN } -} \ No newline at end of file +} diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt index c65bbe25fbbf..50ddb5a3ea67 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt @@ -1,19 +1,19 @@ package app.aaps.core.interfaces.insulin -import androidx.annotation.StringRes import app.aaps.core.data.model.ICfg -import app.aaps.core.interfaces.R +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef -enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeakTime: Long, @StringRes val label: Int, @StringRes val comment: Int) { - UNKNOWN(-1, 0, 0, R.string.unknown, R.string.unknown), +enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeakTime: Long, val label: TextRef, val comment: TextRef) { + UNKNOWN(-1, 0, 0, InterfacesStrings.unknown, InterfacesStrings.unknown), // int FAST_ACTING_INSULIN = 0; // old model no longer available // int FAST_ACTING_INSULIN_PROLONGED = 1; // old model no longer available - OREF_RAPID_ACTING(2, 8 * 3600 * 1000, 75 * 60000, R.string.rapid_acting_oref, R.string.fast_acting_insulin_comment), - OREF_ULTRA_RAPID_ACTING(3, 8 * 3600 * 1000, 55 * 60000, R.string.ultra_rapid_oref, R.string.ultra_fast_acting_insulin_comment), - OREF_FREE_PEAK(4, 8 * 3600 * 1000, 50 * 60000, R.string.free_peak_oref, R.string.insulin_peak_time), - OREF_LYUMJEV(5, 8 * 3600 * 1000, 45 * 60000, R.string.lyumjev, R.string.lyumjev); + OREF_RAPID_ACTING(2, 8 * 3600 * 1000, 75 * 60000, InterfacesStrings.rapid_acting_oref, InterfacesStrings.fast_acting_insulin_comment), + OREF_ULTRA_RAPID_ACTING(3, 8 * 3600 * 1000, 55 * 60000, InterfacesStrings.ultra_rapid_oref, InterfacesStrings.ultra_fast_acting_insulin_comment), + OREF_FREE_PEAK(4, 8 * 3600 * 1000, 50 * 60000, InterfacesStrings.free_peak_oref, InterfacesStrings.insulin_peak_time), + OREF_LYUMJEV(5, 8 * 3600 * 1000, 45 * 60000, InterfacesStrings.lyumjev, InterfacesStrings.lyumjev); val iCfg: ICfg get() = ICfg(this.name, insulinEndTime, insulinPeakTime, 1.0) @@ -24,7 +24,7 @@ enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeak companion object { private val map = entries.associateBy(InsulinType::value) - fun fromInt(type: Int) = map[type] ?:OREF_RAPID_ACTING - fun fromPeak(insulinPeakTime: Long) = values().firstOrNull {it.insulinPeakTime == insulinPeakTime} ?:OREF_FREE_PEAK + fun fromInt(type: Int) = map[type] ?: OREF_RAPID_ACTING + fun fromPeak(insulinPeakTime: Long) = entries.firstOrNull { it.insulinPeakTime == insulinPeakTime } ?: OREF_FREE_PEAK } -} \ No newline at end of file +} diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index e4687cec3c49..70a8be68a55c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.RawRes import androidx.annotation.StringRes +import app.aaps.core.interfaces.InterfacesStringIds import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs @@ -78,14 +79,17 @@ interface ResourceHelper { /** * Resolves a [TextRef.Named] that this module can see. * - * `:core:interfaces` depends on `:core:keys` and nothing else that owns strings, so only `keys` is - * resolvable here. A name owned by another module falls back to showing the raw name - visibly wrong - * rather than silently blank. + * Two owners are resolvable here: `keys`, via the `:core:keys` dependency, and `interfaces`, whose + * map is generated into this module. A name owned by another module falls back to showing the raw + * name - visibly wrong rather than silently blank. * * That is not a gap in practice today: the `ui`-owned names are used from Composables, and - * `app.aaps.core.ui.compose.stringResource` sits in `:core:ui`, which can see both maps. If a - * non-Compose caller ever needs a `ui` name, this is the place that has to learn about it - probably - * as a registry rather than another branch. + * `app.aaps.core.ui.compose.stringResource` sits in `:core:ui`, which can see all three maps. If a + * non-Compose caller ever needs a `ui` name, this is the place that has to learn about it - at that + * point a registry is probably better than a third branch. */ -private fun keysIdOf(ref: TextRef.Named): Int? = - if (ref.owner == "keys") KeysStringIds.idOf(ref.name) else null +private fun keysIdOf(ref: TextRef.Named): Int? = when (ref.owner) { + "keys" -> KeysStringIds.idOf(ref.name) + "interfaces" -> InterfacesStringIds.idOf(ref.name) + else -> null +} diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt index 1f1f7e57d7f2..24c797b4344d 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt @@ -1,37 +1,37 @@ package app.aaps.core.interfaces.rx.weardata -import androidx.annotation.StringRes -import app.aaps.core.interfaces.R +import app.aaps.core.interfaces.InterfacesStrings +import app.aaps.core.keys.interfaces.TextRef -enum class CwfMetadataKey(val key: String, @StringRes val label: Int, val isPref: Boolean) { +enum class CwfMetadataKey(val key: String, val label: TextRef, val isPref: Boolean) { - CWF_NAME("name", R.string.metadata_label_watchface_name, false), - CWF_FILENAME("filename", R.string.metadata_wear_import_filename, false), - CWF_AUTHOR("author", R.string.metadata_label_watchface_author, false), - CWF_CREATED_AT("created_at", R.string.metadata_label_watchface_created_at, false), - CWF_VERSION("cwf_version", R.string.metadata_label_plugin_version, false), - CWF_AUTHOR_VERSION("author_version", R.string.metadata_label_watchface_name_version, false), - CWF_COMMENT("comment", R.string.metadata_label_watchface_infos, false), - CWF_AUTHORIZATION("cwf_authorization", R.string.metadata_label_watchface_authorization, false), - CWF_PREF_WATCH_SHOW_DETAILED_IOB("key_show_detailed_iob", R.string.pref_show_detailed_iob, true), - CWF_PREF_WATCH_SHOW_DETAILED_DELTA("key_show_detailed_delta", R.string.pref_show_detailed_delta, true), - CWF_PREF_WATCH_SHOW_BGI("key_show_bgi", R.string.pref_show_bgi, true), - CWF_PREF_WATCH_SHOW_IOB("key_show_iob", R.string.pref_show_iob, true), - CWF_PREF_WATCH_SHOW_COB("key_show_cob", R.string.pref_show_cob, true), - CWF_PREF_WATCH_SHOW_DELTA("key_show_delta", R.string.pref_show_delta, true), - CWF_PREF_WATCH_SHOW_AVG_DELTA("key_show_avg_delta", R.string.pref_show_avgdelta, true), - CWF_PREF_WATCH_SHOW_TEMP_TARGET("key_show_temp_target", R.string.pref_show_tempTarget ,true), - CWF_PREF_WATCH_SHOW_RESERVOIR_LEVEL("key_show_reservoir_level", R.string.pref_show_reservoir_level ,true), - CWF_PREF_WATCH_SHOW_UPLOADER_BATTERY("key_show_uploader_battery", R.string.pref_show_phone_battery, true), - CWF_PREF_WATCH_SHOW_RIG_BATTERY("key_show_rig_battery", R.string.pref_show_rig_battery, true), - CWF_PREF_WATCH_SHOW_TEMP_BASAL("key_show_temp_basal", R.string.pref_show_basal_rate, true), - CWF_PREF_WATCH_SHOW_DIRECTION("key_show_direction", R.string.pref_show_direction_arrow, true), - CWF_PREF_WATCH_SHOW_AGO("key_show_ago", R.string.pref_show_ago, true), - CWF_PREF_WATCH_SHOW_BG("key_show_bg", R.string.pref_show_bg, true), - CWF_PREF_WATCH_SHOW_LOOP_STATUS("key_show_loop_status", R.string.pref_show_loop_status, true), - CWF_PREF_WATCH_SHOW_WEEK_NUMBER("key_show_week_number", R.string.pref_show_week_number, true), - CWF_PREF_WATCH_SHOW_DATE("key_show_date", R.string.pref_show_date, true), - CWF_PREF_WATCH_SHOW_SECONDS("key_show_seconds", R.string.pref_show_seconds, true); + CWF_NAME("name", InterfacesStrings.metadata_label_watchface_name, false), + CWF_FILENAME("filename", InterfacesStrings.metadata_wear_import_filename, false), + CWF_AUTHOR("author", InterfacesStrings.metadata_label_watchface_author, false), + CWF_CREATED_AT("created_at", InterfacesStrings.metadata_label_watchface_created_at, false), + CWF_VERSION("cwf_version", InterfacesStrings.metadata_label_plugin_version, false), + CWF_AUTHOR_VERSION("author_version", InterfacesStrings.metadata_label_watchface_name_version, false), + CWF_COMMENT("comment", InterfacesStrings.metadata_label_watchface_infos, false), + CWF_AUTHORIZATION("cwf_authorization", InterfacesStrings.metadata_label_watchface_authorization, false), + CWF_PREF_WATCH_SHOW_DETAILED_IOB("key_show_detailed_iob", InterfacesStrings.pref_show_detailed_iob, true), + CWF_PREF_WATCH_SHOW_DETAILED_DELTA("key_show_detailed_delta", InterfacesStrings.pref_show_detailed_delta, true), + CWF_PREF_WATCH_SHOW_BGI("key_show_bgi", InterfacesStrings.pref_show_bgi, true), + CWF_PREF_WATCH_SHOW_IOB("key_show_iob", InterfacesStrings.pref_show_iob, true), + CWF_PREF_WATCH_SHOW_COB("key_show_cob", InterfacesStrings.pref_show_cob, true), + CWF_PREF_WATCH_SHOW_DELTA("key_show_delta", InterfacesStrings.pref_show_delta, true), + CWF_PREF_WATCH_SHOW_AVG_DELTA("key_show_avg_delta", InterfacesStrings.pref_show_avgdelta, true), + CWF_PREF_WATCH_SHOW_TEMP_TARGET("key_show_temp_target", InterfacesStrings.pref_show_tempTarget, true), + CWF_PREF_WATCH_SHOW_RESERVOIR_LEVEL("key_show_reservoir_level", InterfacesStrings.pref_show_reservoir_level, true), + CWF_PREF_WATCH_SHOW_UPLOADER_BATTERY("key_show_uploader_battery", InterfacesStrings.pref_show_phone_battery, true), + CWF_PREF_WATCH_SHOW_RIG_BATTERY("key_show_rig_battery", InterfacesStrings.pref_show_rig_battery, true), + CWF_PREF_WATCH_SHOW_TEMP_BASAL("key_show_temp_basal", InterfacesStrings.pref_show_basal_rate, true), + CWF_PREF_WATCH_SHOW_DIRECTION("key_show_direction", InterfacesStrings.pref_show_direction_arrow, true), + CWF_PREF_WATCH_SHOW_AGO("key_show_ago", InterfacesStrings.pref_show_ago, true), + CWF_PREF_WATCH_SHOW_BG("key_show_bg", InterfacesStrings.pref_show_bg, true), + CWF_PREF_WATCH_SHOW_LOOP_STATUS("key_show_loop_status", InterfacesStrings.pref_show_loop_status, true), + CWF_PREF_WATCH_SHOW_WEEK_NUMBER("key_show_week_number", InterfacesStrings.pref_show_week_number, true), + CWF_PREF_WATCH_SHOW_DATE("key_show_date", InterfacesStrings.pref_show_date, true), + CWF_PREF_WATCH_SHOW_SECONDS("key_show_seconds", InterfacesStrings.pref_show_seconds, true); companion object { @@ -40,4 +40,4 @@ enum class CwfMetadataKey(val key: String, @StringRes val label: Int, val isPref } } -typealias CwfMetadataMap = MutableMap \ No newline at end of file +typealias CwfMetadataMap = MutableMap diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt index 4c1799b589d0..8c985d26754d 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -2,6 +2,7 @@ package app.aaps.core.ui.compose import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import app.aaps.core.interfaces.InterfacesStringIds import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs @@ -42,13 +43,15 @@ fun stringResource(ref: TextRef): String = when (ref) { * maps in some order - `ns_wifi_ssids` exists in both with different translations, and guessing * would silently pick one. * - * `:core:ui` can see both maps because it depends on `:core:keys`. A module that converts later adds - * its own branch here, or this becomes a registry once there are enough of them to be worth one. + * `:core:ui` can see all three maps because it depends on `:core:keys` and `:core:interfaces`. A + * module that converts later adds its own branch here, or this becomes a registry once there are + * enough of them to be worth one. */ private fun androidIdOf(ref: TextRef.Named): Int? = when (ref.owner) { - "keys" -> KeysStringIds.idOf(ref.name) - "ui" -> UiStringIds.idOf(ref.name) - else -> null + "keys" -> KeysStringIds.idOf(ref.name) + "ui" -> UiStringIds.idOf(ref.name) + "interfaces" -> InterfacesStringIds.idOf(ref.name) + else -> null } /** diff --git a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt index 5113e6c0ced8..73a940b4d525 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt @@ -14,6 +14,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -76,6 +77,16 @@ class InsulinImplMigrationTest : TestBase() { // Deterministic, unique string per resource id — avoids depending on real translations while // still letting us assert "nickname == template label" by calling the same stub. whenever(rh.gs(any())).thenAnswer { "S" + it.getArgument(0) } + // InsulinType.label is a TextRef now, and gs(TextRef) is a DEFAULT interface method: a mock + // intercepts it and returns null instead of running the body that would delegate to gs(id). + // So it needs its own stub, following the same "unique string per reference" rule. + whenever(rh.gs(any())).thenAnswer { + when (val ref = it.getArgument(0)) { + is TextRef.Named -> "S" + ref.name + is TextRef.AndroidRes -> "S" + ref.id + is TextRef.Literal -> ref.text + } + } whenever(preferences.get(StringNonKey.InsulinConfiguration)).thenAnswer { storedConfig } doAnswer { storedConfig = it.getArgument(1); null } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt index 1f8012585fa0..51637ae5e91f 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt @@ -8,6 +8,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -48,6 +49,14 @@ class InsulinImplSyncTest : TestBase() { fun setup() { whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) whenever(rh.gs(any())).thenAnswer { "S" + it.getArgument(0) } + // gs(TextRef) is a DEFAULT interface method, so a mock returns null rather than running it. + whenever(rh.gs(any())).thenAnswer { + when (val ref = it.getArgument(0)) { + is TextRef.Named -> "S" + ref.name + is TextRef.AndroidRes -> "S" + ref.id + is TextRef.Literal -> ref.text + } + } whenever(config.AAPSCLIENT).thenReturn(true) whenever(preferences.observe(StringNonKey.InsulinConfiguration)).thenReturn(configFlow) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt index 0f03c0f72380..7a6dba28bb7d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt @@ -259,8 +259,8 @@ class WearViewModel @Inject constructor( val items = files.map { cwfFile -> val metadata = cwfFile.cwfData.metadata val name = metadata[CwfMetadataKey.CWF_AUTHOR_VERSION]?.let { av -> - rh.gs(CwfMetadataKey.CWF_AUTHOR_VERSION.label, metadata[CwfMetadataKey.CWF_NAME], av) - } ?: rh.gs(CwfMetadataKey.CWF_NAME.label, metadata[CwfMetadataKey.CWF_NAME]) + rh.gs(CwfMetadataKey.CWF_AUTHOR_VERSION.label, metadata[CwfMetadataKey.CWF_NAME] ?: "", av) + } ?: rh.gs(CwfMetadataKey.CWF_NAME.label, metadata[CwfMetadataKey.CWF_NAME] ?: "") val fileName = metadata[CwfMetadataKey.CWF_FILENAME]?.let { "$it.${ZipWatchfaceFormat.CWF_EXTENSION}" } ?: "" CwfImportItemState( cwfFile = cwfFile, diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt index 310f805522f0..66a9ae9a1f96 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt @@ -13,6 +13,7 @@ import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.versionChecker.VersionCheckerUtils import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.R import app.aaps.plugins.sync.wear.WearPlugin import com.google.common.truth.Truth.assertThat @@ -185,6 +186,9 @@ internal class WearViewModelTest { // Mock resource helper calls whenever(rh.gs(any(), any())).thenReturn("mocked") + // CwfMetadataKey.label is a TextRef now; gs(TextRef, vararg) is a default interface method, + // which a mock intercepts and answers with null unless it is stubbed here too. + whenever(rh.gs(any(), any())).thenReturn("mocked") whenever(versionCheckerUtils.versionDigits(any())).thenReturn(intArrayOf(1, 0, 0)) sut.showCwfInfos() diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinCarouselCard.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinCarouselCard.kt index 60633cb37fb2..528dcde4b58a 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinCarouselCard.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinCarouselCard.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt index 69afbdfd7360..2a6607df4393 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver From 8e928f303674666ee0c6f314ddb661dcde313161 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 21:15:09 +0200 Subject: [PATCH 042/146] Shared org.json compat shim with parity tests --- core/data/build.gradle.kts | 8 + .../app/aaps/core/data/json/OrgJsonCompat.kt | 146 ++++++++++ .../core/data/json/OrgJsonCompatParityTest.kt | 253 ++++++++++++++++++ gradle/libs.versions.toml | 6 + .../clientcontrol/ClientControlReceiver.kt | 4 +- .../clientcontrol/ClientControlRoundTrip.kt | 2 +- .../clientcontrol/PairingOfferFetcher.kt | 4 +- .../sync/nsclientV3/json/OrgJsonCompat.kt | 93 ------- .../nsclientV3/workers/LoadSettingsWorker.kt | 2 +- .../sync/nsclientV3/json/OrgJsonCompatTest.kt | 117 +------- 10 files changed, 428 insertions(+), 207 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/json/OrgJsonCompat.kt create mode 100644 core/data/src/jvmTest/kotlin/app/aaps/core/data/json/OrgJsonCompatParityTest.kt delete mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e53ea7547ee7..2d32e5d11e54 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -28,6 +28,12 @@ kotlin { // expect/actual, and the Native actual scopes its own opt-in to one file. sourceSets { + commonMain { + dependencies { + api(project.dependencies.platform(libs.kotlinx.serialization.bom)) + api(libs.kotlinx.serialization.json) + } + } getByName("commonTest") { dependencies { implementation(kotlin("test")) @@ -38,6 +44,8 @@ kotlin { implementation(libs.org.junit.jupiter) implementation(libs.com.google.truth) runtimeOnly(libs.org.junit.platform.launcher) + // The oracle for OrgJsonCompatParityTest, and the only place org.json may appear. + implementation(libs.org.json.android) } } } diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/json/OrgJsonCompat.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/json/OrgJsonCompat.kt new file mode 100644 index 000000000000..ed3ef53d9c64 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/json/OrgJsonCompat.kt @@ -0,0 +1,146 @@ +package app.aaps.core.data.json + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull + +/** + * Accessors on kotlinx [JsonObject] that behave exactly like the `org.json` ones they replace. + * + * `org.json` is a JVM and Android API, so it cannot go to iOS. Code moving to `commonMain` has to + * stop using it, but the two libraries disagree about a missing key and nothing about that + * disagreement shows up at compile time: + * + * ``` + * doc.optString("k") // org.json : "" when absent, never null + * doc["k"]?.jsonPrimitive?.content // kotlinx : null when absent + * ``` + * + * A straight translation would silently flip every downstream `isEmpty()` check and `?:` fallback. + * These functions keep the old behaviour so the type swap changes nothing that can be observed, and + * `OrgJsonCompatParityTest` checks each one against a real `org.json` over a shared matrix of inputs. + * + * **This is a migration shim, not a design.** It deliberately copies quirks that are probably bugs - + * see [optStringCompat] returning the four letter text `null`. They are kept so the move off + * `org.json` is provably behaviour preserving. Fixing them is a separate, visible change. + * + * It used to live in `:plugins:sync`, which is Android only, on the argument that the quirks should + * stay out of shared modules. That argument stopped holding when `:core:interfaces` itself had to + * move: the profile spine (`Profile.toPureNsJson`, `ProfileStore`, `SingleProfile`) carries + * `JSONObject` and `JSONArray` in its **contracts**, so the shim has to be visible wherever those + * contracts are. + * + * Read this together with the divergences the parity test documents but does NOT hide - see the + * `writes differ` section there. Reading is safe to shim; writing changes the bytes on the wire. + */ +object OrgJsonCompat { + + /** + * Same as `org.json.JSONObject.optString(name)`. + * + * - missing key -> `""` + * - explicit JSON null -> the four letter text `"null"`, because `org.json` renders its + * `JSONObject.NULL` sentinel through `String.valueOf` + * - string -> the string itself, unquoted + * - number or boolean -> its text form + * - object or array -> its JSON text + * + * Never returns Kotlin null. Callers written against `org.json` rely on that, sometimes without + * meaning to. + */ + fun JsonObject.optStringCompat(key: String): String { + val element = this[key] ?: return "" + return when (element) { + is JsonNull -> "null" + is JsonPrimitive -> element.content + else -> element.toString() + } + } + + /** + * Same as `org.json.JSONObject.optJSONObject(name)`: null when the key is missing, when the + * value is an explicit JSON null, or when it holds anything that is not an object. + */ + fun JsonObject.optJsonObjectCompat(key: String): JsonObject? = this[key] as? JsonObject + + /** + * Same as `org.json.JSONObject.optLong(name, fallback)`. + * + * Coerces like `org.json` does: a numeric string is parsed, and a floating point value is + * truncated toward zero. Anything that cannot be read as a number gives [fallback]. + */ + fun JsonObject.optLongCompat(key: String, fallback: Long): Long { + val primitive = this[key] as? JsonPrimitive ?: return fallback + if (primitive is JsonNull) return fallback + primitive.longOrNull?.let { return it } + primitive.doubleOrNull?.let { return it.toLong() } + return fallback + } + + /** + * Same as `org.json.JSONObject.optBoolean(name)`: false unless the value is a real `true`, or + * the text `"true"` in any case. `org.json` accepts the string form too. + */ + fun JsonObject.optBooleanCompat(key: String): Boolean { + val primitive = this[key] as? JsonPrimitive ?: return false + if (primitive is JsonNull) return false + primitive.booleanOrNull?.let { return it } + return primitive.content.equals("true", ignoreCase = true) + } + + /** Same as `org.json.JSONObject.optJSONArray(name)`. */ + fun JsonObject.optJsonArrayCompat(key: String): JsonArray? = this[key] as? JsonArray + + /** + * Same as `org.json.JSONObject.optDouble(name, fallback)`. + * + * Needed by the profile spine, where every schedule value is a double. Coerces a numeric string + * the way `org.json` does. + */ + fun JsonObject.optDoubleCompat(key: String, fallback: Double): Double { + val primitive = this[key] as? JsonPrimitive ?: return fallback + if (primitive is JsonNull) return fallback + return primitive.doubleOrNull ?: fallback + } + + /** + * Same as `org.json.JSONObject.optInt(name, fallback)`, including its inconsistency about + * out-of-range values - which is why this reads more carefully than the others. + * + * `org.json` takes two different routes depending on how the value was written, and they + * disagree once the value does not fit in an `Int`: + * + * - a real JSON **number** is a boxed `Number`, and `org.json` calls `intValue()` on it, which + * TRUNCATES the low 32 bits - so `1785992181588` reads back as `-714213548` + * - a **quoted** number is text, and `org.json` falls back to `Double.intValue()`, which + * SATURATES - so `"1785992181588"` reads back as `2147483647` + * + * Reading both through `Long.toInt()` looks obviously right and is wrong for the quoted case: + * it silently turns a large positive number negative. The parity test catches exactly that. + */ + fun JsonObject.optIntCompat(key: String, fallback: Int): Int { + val primitive = this[key] as? JsonPrimitive ?: return fallback + if (primitive is JsonNull) return fallback + return if (primitive.isString) { + primitive.content.toIntOrNull() + ?: primitive.content.toDoubleOrNull()?.toInt() // saturates, like Double.intValue() + ?: fallback + } else { + primitive.longOrNull?.toInt() // truncates, like Long.intValue() + ?: primitive.doubleOrNull?.toInt() + ?: fallback + } + } + + /** + * Same as `org.json.JSONObject.has(name)`. + * + * Note this is `has`, not "has a usable value": `org.json` answers true for an explicit JSON + * null, and so does this. + */ + fun JsonObject.hasCompat(key: String): Boolean = containsKey(key) +} diff --git a/core/data/src/jvmTest/kotlin/app/aaps/core/data/json/OrgJsonCompatParityTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/json/OrgJsonCompatParityTest.kt new file mode 100644 index 000000000000..058399cb9296 --- /dev/null +++ b/core/data/src/jvmTest/kotlin/app/aaps/core/data/json/OrgJsonCompatParityTest.kt @@ -0,0 +1,253 @@ +package app.aaps.core.data.json + +import app.aaps.core.data.json.OrgJsonCompat.hasCompat +import app.aaps.core.data.json.OrgJsonCompat.optBooleanCompat +import app.aaps.core.data.json.OrgJsonCompat.optDoubleCompat +import app.aaps.core.data.json.OrgJsonCompat.optIntCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonArrayCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.core.data.json.OrgJsonCompat.optLongCompat +import app.aaps.core.data.json.OrgJsonCompat.optStringCompat +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import org.json.JSONObject +import org.junit.jupiter.api.Test + +/** + * Golden master for [OrgJsonCompat], plus a written record of where the two libraries genuinely + * disagree. + * + * Every read case below is parsed **twice**, once with `org.json` and once with kotlinx, and the two + * results are required to match. `org.json` is the reference: whatever it does today is what the + * replacement has to keep doing, quirks included. The accessor differences are invisible to the + * compiler - if `optString` starts returning null for a missing key nothing fails to build, a + * downstream `isEmpty()` just quietly takes the other branch - and this is what makes that visible. + * + * **The oracle is `org.json:json` from Maven, which is NOT byte for byte the same implementation as + * Android's.** Android ships a Harmony derived `org.json`; Maven ships Crockford's. They agree on + * every accessor exercised here, which is why this test is worth running, but do not extend it to + * anything where the two implementations could reasonably differ (iteration order is the known one) + * without checking on a device first. + * + * The second half is deliberately different in kind. It does not assert that the libraries agree - + * it asserts that they do NOT, and pins exactly how. Those are the cases a migration has to shim or + * accept, and a test that quietly skipped them would be worse than no test. + */ +class OrgJsonCompatParityTest { + + /** One row of the matrix: a document, and the key to read out of it. */ + private data class Case(val name: String, val json: String, val key: String) + + private val cases = listOf( + Case("missing key", """{"other":1}""", "k"), + Case("explicit null", """{"k":null}""", "k"), + Case("plain string", """{"k":"hello"}""", "k"), + Case("empty string", """{"k":""}""", "k"), + Case("string with spaces", """{"k":"a b c"}""", "k"), + Case("string that looks numeric", """{"k":"1785992181588"}""", "k"), + Case("string true", """{"k":"true"}""", "k"), + Case("string TRUE upper", """{"k":"TRUE"}""", "k"), + Case("string false", """{"k":"false"}""", "k"), + Case("int", """{"k":42}""", "k"), + Case("zero", """{"k":0}""", "k"), + Case("negative int", """{"k":-6}""", "k"), + Case("big long", """{"k":1785992181588}""", "k"), + Case("double", """{"k":0.692}""", "k"), + Case("negative double", """{"k":-0.411}""", "k"), + Case("integral double", """{"k":1.0}""", "k"), + Case("boolean true", """{"k":true}""", "k"), + Case("boolean false", """{"k":false}""", "k"), + Case("nested object", """{"k":{"a":1}}""", "k"), + Case("empty object", """{"k":{}}""", "k"), + Case("array", """{"k":[1,2,3]}""", "k"), + Case("empty array", """{"k":[]}""", "k") + ) + + private fun orgJson(case: Case) = JSONObject(case.json) + private fun kotlinxJson(case: Case): JsonObject = Json.parseToJsonElement(case.json).jsonObject + + // ---------------------------------------------------------------- reads must match + + @Test + fun `optString matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optStringCompat(case.key)) + .isEqualTo(orgJson(case).optString(case.key)) + } + } + + @Test + fun `optLong matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optLongCompat(case.key, 0L)) + .isEqualTo(orgJson(case).optLong(case.key, 0L)) + } + } + + @Test + fun `optLong honours a non zero fallback for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optLongCompat(case.key, -1L)) + .isEqualTo(orgJson(case).optLong(case.key, -1L)) + } + } + + @Test + fun `optDouble matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optDoubleCompat(case.key, 0.0)) + .isEqualTo(orgJson(case).optDouble(case.key, 0.0)) + } + } + + @Test + fun `optInt matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optIntCompat(case.key, 0)) + .isEqualTo(orgJson(case).optInt(case.key, 0)) + } + } + + @Test + fun `optBoolean matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optBooleanCompat(case.key)) + .isEqualTo(orgJson(case).optBoolean(case.key)) + } + } + + @Test + fun `has matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).hasCompat(case.key)) + .isEqualTo(orgJson(case).has(case.key)) + } + } + + @Test + fun `optJSONObject presence matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optJsonObjectCompat(case.key) != null) + .isEqualTo(orgJson(case).optJSONObject(case.key) != null) + } + } + + @Test + fun `optJSONArray presence matches org json for every case`() { + cases.forEach { case -> + assertWithMessage(case.name) + .that(kotlinxJson(case).optJsonArrayCompat(case.key) != null) + .isEqualTo(orgJson(case).optJSONArray(case.key) != null) + } + } + + // ---------------------------------------------------------------- writes differ + // + // Everything above says "these agree, rely on it". Everything below says "these do NOT agree, + // and here is exactly how" - because the profile spine SERIALIZES (Profile.toPureNsJson feeds + // Nightscout), and a value that reads back fine can still go onto the wire in a different shape. + + /** + * The one that would actually change bytes sent to Nightscout. + * + * `org.json` renders a whole-numbered double WITHOUT a fractional part, so a basal rate of + * `1.0` is uploaded as `1`. kotlinx keeps the double it was given and writes `1.0`. Both parse + * back to the same number, so nothing round-trips wrong inside AAPS - but the uploaded document + * is not byte identical, and anything comparing documents as text (a sync hash, a diff, a + * strict consumer) would see every whole basal rate change. + */ + @Test + fun `whole numbered doubles serialize differently`() { + val org = JSONObject().put("basal", 1.0).toString() + val kotlinx = buildJsonObject { put("basal", JsonPrimitive(1.0)) }.toString() + + assertThat(org).isEqualTo("""{"basal":1}""") + assertThat(kotlinx).isEqualTo("""{"basal":1.0}""") + assertWithMessage("if this ever passes, the divergence is gone and the shim can drop it") + .that(org).isNotEqualTo(kotlinx) + } + + /** A non-whole double is written the same way by both, so only the integral case needs care. */ + @Test + fun `fractional doubles serialize identically`() { + val org = JSONObject().put("basal", 0.825).toString() + val kotlinx = buildJsonObject { put("basal", JsonPrimitive(0.825)) }.toString() + assertThat(org).isEqualTo(kotlinx) + } + + /** + * `org.json` refuses a non finite double outright; kotlinx accepts it and emits text that is not + * valid JSON. A corrupt profile value therefore fails loudly today and would fail silently after + * a naive migration - the receiver is what would break, and much later. + */ + @Test + fun `non finite doubles are rejected by org json but not by kotlinx`() { + var orgThrew = false + try { + JSONObject().put("k", Double.NaN) + } catch (_: Exception) { + orgThrew = true + } + assertThat(orgThrew).isTrue() + + val kotlinx = buildJsonObject { put("k", JsonPrimitive(Double.NaN)) }.toString() + assertThat(kotlinx).isEqualTo("""{"k":NaN}""") + } + + /** + * `org.json` accepts input that is not JSON - single quotes, unquoted keys - because it was + * written to be forgiving. kotlinx rejects it unless explicitly made lenient. + * + * This matters for anything read from outside AAPS: a hand-edited profile, an old export, a + * third party uploader. Strictness is better, but it is a behaviour change, not a free win. + */ + @Test + fun `org json accepts malformed input that kotlinx rejects`() { + val malformed = """{k:"v"}""" // unquoted key - not JSON + + assertThat(JSONObject(malformed).optString("k")).isEqualTo("v") + + var kotlinxThrew = false + try { + Json.parseToJsonElement(malformed) + } catch (_: Exception) { + kotlinxThrew = true + } + assertWithMessage("kotlinx must reject what org.json tolerated").that(kotlinxThrew).isTrue() + + // ...and it can be made to accept it, if a call site turns out to need that. + val lenient = Json { isLenient = true } + assertThat(lenient.parseToJsonElement(malformed).jsonObject.optStringCompat("k")).isEqualTo("v") + } + + /** + * kotlinx preserves insertion order; this `org.json` does not guarantee it. + * + * Pinned only as "kotlinx is ordered", NOT as a comparison, because iteration order is exactly + * where the Maven and Android implementations are allowed to differ - asserting the oracle's + * order here would pin the wrong library's behaviour. + */ + @Test + fun `kotlinx preserves insertion order`() { + val doc = buildJsonObject { + put("z", JsonPrimitive(1)) + put("a", JsonPrimitive(2)) + put("m", JsonPrimitive(3)) + } + assertThat(doc.keys.toList()).containsExactly("z", "a", "m").inOrder() + assertThat(doc.toString()).isEqualTo("""{"z":1,"a":2,"m":3}""") + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e62004979e8a..579a634686eb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -180,6 +180,12 @@ com-caverock-androidsvg = { group = "com.caverock", name = "androidsvg", version net-danlew-android-joda = { group = "net.danlew", name = "android.joda", version = "2.14.2.1" } joda-time = { group = "joda-time", name = "joda-time", version = "2.14.3" } org-skyscreamer-jsonassert = { group = "org.skyscreamer", name = "jsonassert", version = "1.5.3" } +# Test-only oracle. On Android `org.json` comes from the platform, so a plain JVM test has no real +# implementation to compare against. This is AOSP's own org.json, repackaged for the JVM - NOT +# Crockford's `org.json:json`, which is a different implementation and disagrees on real cases +# (optString of a JSON null gives "" there and "null" on Android). It is what jsonassert already +# pulls in, so Android module tests are measured against this too. Never use it in main source. +org-json-android = { group = "com.vaadin.external.google", name = "android-json", version = "0.0.20131108.vaadin1" } com-eatthepath-java-otp = { group = "com.eatthepath", name = "java-otp", version = "0.4.0" } com-github-bumptech-glide-compose = { group = "com.github.bumptech.glide", name = "compose", version = "1.0.0-beta10" } # do not update to 4.xx com-github-kenglxn-qrgen-android = { group = "com.github.kenglxn.QRGen", name = "android", version = "3.0.1" } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt index b69b1bded3de..43082f316875 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt @@ -43,8 +43,8 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.SignedEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.WizardDetailDto import app.aaps.core.nssdk.utils.ClientControlCrypto import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.core.data.json.OrgJsonCompat.optStringCompat import app.aaps.plugins.sync.nsclientV3.services.RunningConfigurationPublisher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt index 82db9d5245cb..276e60dad413 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt @@ -33,7 +33,7 @@ import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.PROGRESS_WATCHDOG_MS import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.PROPAGATION_MARGIN_MS import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip.Companion.ROUND_TRIP_TTL_MS -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonObjectCompat import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt index ba3b14ab5b7e..379cc8d680e1 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/PairingOfferFetcher.kt @@ -9,8 +9,8 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.PairingOffer import app.aaps.core.nssdk.localmodel.clientcontrol.PairingPayload import app.aaps.core.nssdk.utils.ClientControlPairingCrypto import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.core.data.json.OrgJsonCompat.optStringCompat import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt deleted file mode 100644 index 24407ba9a794..000000000000 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompat.kt +++ /dev/null @@ -1,93 +0,0 @@ -package app.aaps.plugins.sync.nsclientV3.json - -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.longOrNull - -/** - * Accessors on kotlinx [JsonObject] that behave exactly like the `org.json` ones they replace. - * - * `org.json` is a JVM and Android API, so it cannot go to iOS. The NSClientV3 wire layer is moving - * to kotlinx [JsonObject], but the two libraries disagree about a missing key and nothing about that - * disagreement shows up at compile time: - * - * ``` - * doc.optString("k") // org.json : "" when absent, never null - * doc["k"]?.jsonPrimitive?.content // kotlinx : null when absent - * ``` - * - * A straight translation would silently flip every downstream `isEmpty()` check and `?:` fallback. - * These functions keep the old behaviour so the type swap changes nothing that can be observed, - * and `OrgJsonCompatTest` checks each one against the real `org.json` over a shared matrix of - * inputs. - * - * **This is a migration shim, not a design.** It deliberately copies quirks that are probably bugs - - * see [optString] returning the four letter text `null`. They are kept so the move off `org.json` - * is provably behaviour preserving. Fixing them is a separate, visible change. - * - * It lives in `:plugins:sync` on purpose: this module is Android only (WorkManager, socket.io - * Service) and will never build for iOS, so the `org.json` quirks stay out of the shared modules. - */ -object OrgJsonCompat { - - /** - * Same as `org.json.JSONObject.optString(name)`. - * - * - missing key -> `""` - * - explicit JSON null -> the four letter text `"null"`, because Android's `org.json` renders - * its `JSONObject.NULL` sentinel through `String.valueOf` - * - string -> the string itself, unquoted - * - number or boolean -> its text form - * - object or array -> its JSON text - * - * Never returns Kotlin null. Callers written against `org.json` rely on that, sometimes without - * meaning to: `NSClientV3Service.onDataDelete` guards with `?: return@Listener`, which can never - * fire today. - */ - fun JsonObject.optStringCompat(key: String): String { - val element = this[key] ?: return "" - return when (element) { - is JsonNull -> "null" - is JsonPrimitive -> element.content - else -> element.toString() - } - } - - /** - * Same as `org.json.JSONObject.optJSONObject(name)`: null when the key is missing, when the - * value is an explicit JSON null, or when it holds anything that is not an object. - */ - fun JsonObject.optJsonObjectCompat(key: String): JsonObject? = this[key] as? JsonObject - - /** - * Same as `org.json.JSONObject.optLong(name, fallback)`. - * - * Coerces like `org.json` does: a numeric string is parsed, and a floating point value is - * truncated toward zero. Anything that cannot be read as a number gives [fallback]. - */ - fun JsonObject.optLongCompat(key: String, fallback: Long): Long { - val primitive = this[key] as? JsonPrimitive ?: return fallback - if (primitive is JsonNull) return fallback - primitive.longOrNull?.let { return it } - primitive.doubleOrNull?.let { return it.toLong() } - return fallback - } - - /** - * Same as `org.json.JSONObject.optBoolean(name)`: false unless the value is a real `true`, or - * the text `"true"` in any case. `org.json` accepts the string form too. - */ - fun JsonObject.optBooleanCompat(key: String): Boolean { - val primitive = this[key] as? JsonPrimitive ?: return false - if (primitive is JsonNull) return false - primitive.booleanOrNull?.let { return it } - return primitive.content.equals("true", ignoreCase = true) - } - - /** Same as `org.json.JSONObject.optJSONArray(name)`. */ - fun JsonObject.optJsonArrayCompat(key: String): JsonArray? = this[key] as? JsonArray -} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt index 5a1afe247795..2db023862994 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadSettingsWorker.kt @@ -18,7 +18,7 @@ import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.SettingsIdentifiers import app.aaps.plugins.sync.nsclientV3.clientcontrol.OrphanDetector import app.aaps.plugins.sync.nsclientV3.extensions.toRunningConfiguration -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optLongCompat +import app.aaps.core.data.json.OrgJsonCompat.optLongCompat import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt index 63b681756e7f..3cfdc5931b61 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/json/OrgJsonCompatTest.kt @@ -1,125 +1,26 @@ package app.aaps.plugins.sync.nsclientV3.json -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optBooleanCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonArrayCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optJsonObjectCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optLongCompat -import app.aaps.plugins.sync.nsclientV3.json.OrgJsonCompat.optStringCompat +import app.aaps.core.data.json.OrgJsonCompat.optBooleanCompat +import app.aaps.core.data.json.OrgJsonCompat.optJsonObjectCompat +import app.aaps.core.data.json.OrgJsonCompat.optLongCompat +import app.aaps.core.data.json.OrgJsonCompat.optStringCompat import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject import org.json.JSONObject import org.junit.jupiter.api.Test /** - * Golden master test for [OrgJsonCompat]. + * NSClientV3 specific half of the `OrgJsonCompat` checks. * - * Every case below is parsed **twice**, once with `org.json` and once with kotlinx, and the two - * results are required to match. `org.json` is the reference: whatever it does today is what the - * replacement has to keep doing, quirks included. - * - * The point is that the accessor differences are invisible to the compiler. If `optString` starts - * returning null for a missing key nothing fails to build - a downstream `isEmpty()` just quietly - * takes the other branch. This test is what makes that visible. + * The accessor-by-accessor matrix moved to `OrgJsonCompatParityTest` in `:core:data`, next to the + * shim itself, when the shim was shared so `:core:interfaces` could use it. What stays here is what + * is genuinely about **this** module: the document shapes NSClientV3 actually reads, and the two + * call-site behaviours that would break quietly. */ class OrgJsonCompatTest { - /** One row of the matrix: a document, and the key to read out of it. */ - private data class Case(val name: String, val json: String, val key: String) - - private val cases = listOf( - Case("missing key", """{"other":1}""", "k"), - Case("explicit null", """{"k":null}""", "k"), - Case("plain string", """{"k":"hello"}""", "k"), - Case("empty string", """{"k":""}""", "k"), - Case("string with spaces", """{"k":"a b c"}""", "k"), - Case("string that looks numeric", """{"k":"1785992181588"}""", "k"), - Case("string true", """{"k":"true"}""", "k"), - Case("string TRUE upper", """{"k":"TRUE"}""", "k"), - Case("string false", """{"k":"false"}""", "k"), - Case("int", """{"k":42}""", "k"), - Case("zero", """{"k":0}""", "k"), - Case("negative int", """{"k":-6}""", "k"), - Case("big long", """{"k":1785992181588}""", "k"), - Case("double", """{"k":0.692}""", "k"), - Case("negative double", """{"k":-0.411}""", "k"), - Case("boolean true", """{"k":true}""", "k"), - Case("boolean false", """{"k":false}""", "k"), - Case("nested object", """{"k":{"a":1}}""", "k"), - Case("empty object", """{"k":{}}""", "k"), - Case("array", """{"k":[1,2,3]}""", "k"), - Case("empty array", """{"k":[]}""", "k") - ) - - private fun orgJson(case: Case) = JSONObject(case.json) - private fun kotlinxJson(case: Case): JsonObject = Json.parseToJsonElement(case.json).jsonObject - - // ---------------------------------------------------------------- the matrix - - @Test - fun `optString matches org json for every case`() { - for (case in cases) - assertWithMessage("optString - %s", case.name) - .that(kotlinxJson(case).optStringCompat(case.key)) - .isEqualTo(orgJson(case).optString(case.key)) - } - - @Test - fun `optLong matches org json for every case`() { - for (case in cases) - assertWithMessage("optLong 0 - %s", case.name) - .that(kotlinxJson(case).optLongCompat(case.key, 0L)) - .isEqualTo(orgJson(case).optLong(case.key, 0L)) - } - - @Test - fun `optLong honours a non zero fallback for every case`() { - for (case in cases) - assertWithMessage("optLong -1 - %s", case.name) - .that(kotlinxJson(case).optLongCompat(case.key, -1L)) - .isEqualTo(orgJson(case).optLong(case.key, -1L)) - } - - @Test - fun `optBoolean matches org json for every case`() { - for (case in cases) - assertWithMessage("optBoolean - %s", case.name) - .that(kotlinxJson(case).optBooleanCompat(case.key)) - .isEqualTo(orgJson(case).optBoolean(case.key)) - } - - @Test - fun `optJSONObject presence matches org json for every case`() { - for (case in cases) { - val expected = orgJson(case).optJSONObject(case.key) - val actual = kotlinxJson(case).optJsonObjectCompat(case.key) - - assertWithMessage("optJSONObject present - %s", case.name) - .that(actual != null).isEqualTo(expected != null) - if (expected != null && actual != null) - assertWithMessage("optJSONObject keys - %s", case.name) - .that(actual.keys).isEqualTo(expected.keys().asSequence().toSet()) - } - } - - @Test - fun `optJSONArray presence matches org json for every case`() { - for (case in cases) { - val expected = orgJson(case).optJSONArray(case.key) - val actual = kotlinxJson(case).optJsonArrayCompat(case.key) - - assertWithMessage("optJSONArray present - %s", case.name) - .that(actual != null).isEqualTo(expected != null) - if (expected != null && actual != null) - assertWithMessage("optJSONArray size - %s", case.name) - .that(actual.size).isEqualTo(expected.length()) - } - } - - // ---------------------------------------------------------------- real call site shapes - /** * The shapes the NSClientV3 code actually reads, taken from the live call sites, checked against * `org.json` end to end rather than one accessor at a time. From ba3c465093bc5ff8ad0504e7f573ac43b5ba06f6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 21:52:19 +0200 Subject: [PATCH 043/146] :core:interfaces kotlinx-datetime instead of java.time and joda --- core/data/build.gradle.kts | 3 + .../aaps/core/data/datetime/IsoDateParser.kt | 75 ++++++++++ .../data/datetime/IsoDateParserParityTest.kt | 99 +++++++++++++ core/interfaces/build.gradle.kts | 1 + .../kotlin/app/aaps/core/interfaces/aps/RT.kt | 56 +++++--- .../core/interfaces/utils/MidnightTime.kt | 74 ++++++---- .../interfaces/aps/RtIsoStringParityTest.kt | 114 +++++++++++++++ .../utils/MidnightTimeParityTest.kt | 130 ++++++++++++++++++ .../interfaces/utils/MidnightTimeTest.kt | 4 +- 9 files changed, 510 insertions(+), 46 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/datetime/IsoDateParser.kt create mode 100644 core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt create mode 100644 core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt create mode 100644 core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeParityTest.kt diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 2d32e5d11e54..5df10aa2e8e7 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -32,6 +32,7 @@ kotlin { dependencies { api(project.dependencies.platform(libs.kotlinx.serialization.bom)) api(libs.kotlinx.serialization.json) + api(libs.kotlinx.datetime) } } getByName("commonTest") { @@ -46,6 +47,8 @@ kotlin { runtimeOnly(libs.org.junit.platform.launcher) // The oracle for OrgJsonCompatParityTest, and the only place org.json may appear. implementation(libs.org.json.android) + // The oracle for IsoDateParserParityTest - the joda parser being replaced. + implementation(libs.joda.time) } } } diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/datetime/IsoDateParser.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/datetime/IsoDateParser.kt new file mode 100644 index 000000000000..2703987dd125 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/datetime/IsoDateParser.kt @@ -0,0 +1,75 @@ +package app.aaps.core.data.datetime + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toInstant + +/** + * Lenient ISO 8601 parsing, matching what joda's `ISODateTimeFormat.dateTimeParser()` accepted. + * + * joda is a JVM library so it cannot go to iOS, and kotlinx-datetime is multiplatform but strict: + * `Instant.parse` rejects `+0200` without a colon, a value with no offset at all, and a bare date - + * all of which arrive today from Nightscout device status written by other uploaders. + * + * The trick is to take any explicit offset off the end **first**, then parse what is left as a plain + * local value. One small parser then covers every shape rather than needing a format per variant: + * + * - `2026-08-06T04:56:19.555Z`, `...+02:00`, `...+0200`, `...-04:00` -> that exact instant + * - `2026-08-06T04:56:19.555`, `2026-08-06T04:56` -> **local** time, as joda read it + * - `2026-08-06` -> **local** midnight + * - lower case `t` / `z` -> accepted + * - anything else -> null + * + * Returning null rather than a sentinel is deliberate: the two callers want different things from a + * failure. `:core:nssdk` maps it to `0L` so one bad treatment does not abort a whole sync, while + * `RT` throws, because joda threw there and a silent 1970 timestamp inside an APS result would be + * worse than a loud failure. + * + * `IsoDateParserParityTest` pins every shape above against real joda output. + * + * **Knowingly duplicated** in `:core:nssdk` (`RemoteTreatment.parseCreatedAt`). That module has no + * project dependencies at all, and adding one to share ~30 lines would couple a standalone SDK to + * the app's data module. If a third caller appears, revisit that trade. + */ +fun parseIsoToEpochMillisOrNull(isoDateString: String): Long? { + val text = isoDateString.trim().uppercase() + if (text.isEmpty()) return null + + // Peel off a trailing zone designator, so the rest is a plain local date-time. + var local = text + var offset: UtcOffset? = null + if (text.endsWith("Z")) { + local = text.dropLast(1) + offset = UtcOffset.ZERO + } else { + offsetAtEnd.find(text)?.let { match -> + val parsed = runCatching { UtcOffset.parse(withOffsetColon(match.value)) }.getOrNull() + if (parsed != null) { + local = text.substring(0, match.range.first) + offset = parsed + } + } + } + + val zone = TimeZone.currentSystemDefault() + + runCatching { LocalDateTime.parse(local) }.getOrNull()?.let { dateTime -> + val instant = offset?.let { dateTime.toInstant(it) } ?: dateTime.toInstant(zone) + return instant.toEpochMilliseconds() + } + // Date only. joda gave local midnight, and a date without a time never carries an offset. + runCatching { LocalDate.parse(local) }.getOrNull()?.let { date -> + return date.atStartOfDayIn(zone).toEpochMilliseconds() + } + return null +} + +/** A trailing `+HH:MM` / `+HHMM` offset. Anchored so it cannot match the date's own dashes. */ +private val offsetAtEnd = Regex("""[+-]\d{2}:?\d{2}$""") + +/** `+0200` -> `+02:00`; already-correct input is returned unchanged. */ +private fun withOffsetColon(offset: String): String = + if (offset.contains(':')) offset else offset.substring(0, 3) + ":" + offset.substring(3) diff --git a/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt new file mode 100644 index 000000000000..b4550f7cace9 --- /dev/null +++ b/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt @@ -0,0 +1,99 @@ +package app.aaps.core.data.datetime + +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.TimeZone + +/** + * Pins [parseIsoToEpochMillisOrNull] against joda's `ISODateTimeFormat.dateTimeParser()`. + * + * These strings come from **Nightscout device status written by other uploaders**, not only from + * AAPS itself, so the shapes are not hypothetical. A strict parser that rejected one of them would + * not fail loudly - the value would land in 1970 or the whole document would be dropped, and the + * only symptom would be a missing reading. + * + * The zone is pinned to a non-UTC one with daylight saving because the offset-less shapes are + * resolved in the **local** zone: on a UTC machine those cases would agree no matter what the + * implementation did, and the test would prove nothing. + */ +class IsoDateParserParityTest { + + private lateinit var original: TimeZone + + @BeforeEach fun pinZone() { + original = TimeZone.getDefault() + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague")) + } + + @AfterEach fun restoreZone() { + TimeZone.setDefault(original) + } + + private fun joda(text: String): Long = + DateTime.parse(text, ISODateTimeFormat.dateTimeParser()).toDate().time + + /** Shapes joda accepts, which therefore have to keep working. */ + private val accepted = listOf( + "2026-08-06T04:56:19.555Z", + "2026-08-06T04:56:19Z", + "2026-08-06T04:56:19.555+02:00", + "2026-08-06T04:56:19.555+0200", // no colon - kotlinx alone rejects this + "2026-08-06T04:56:19.555-04:00", + "2026-08-06T04:56:19.555-0430", + "2017-11-19T22:50:34.417+0200", // from the historical DateUtil test + "2017-12-03T16:09:25.000Z", + "2017-12-22T00:32:30Z", + "2026-08-06T04:56:19.555", // no offset at all - local + "2026-08-06T04:56:19", // no offset, no fraction - local + "2026-08-06T04:56", // minutes only - local + "2026-01-15T04:56:19.555", // winter, local: offset differs from summer + "2026-08-06", // date only - local midnight + "2026-01-15" + ) + + @Test fun `every shape joda accepts parses to the same instant`() { + accepted.forEach { text -> + assertWithMessage("input %s", text) + .that(parseIsoToEpochMillisOrNull(text)).isEqualTo(joda(text)) + } + } + + @Test fun `lower case t and z are accepted like joda`() { + listOf("2026-08-06t04:56:19.555z", "2026-08-06t04:56:19.555Z").forEach { text -> + assertWithMessage("input %s", text) + .that(parseIsoToEpochMillisOrNull(text)).isEqualTo(joda(text)) + } + } + + @Test fun `surrounding whitespace is tolerated`() { + assertThat(parseIsoToEpochMillisOrNull(" 2026-08-06T04:56:19.555Z ")) + .isEqualTo(joda("2026-08-06T04:56:19.555Z")) + } + + /** + * Proves the offset-less cases above are not vacuous: in Europe/Prague the same wall clock time + * maps to two different instants depending on the time of year, so a parser that ignored the + * local zone would fail one of them. + */ + @Test fun `offset-less values really are resolved in the local zone`() { + val summer = parseIsoToEpochMillisOrNull("2026-08-06T04:56:19.555")!! + val winter = parseIsoToEpochMillisOrNull("2026-01-15T04:56:19.555")!! + val summerUtc = parseIsoToEpochMillisOrNull("2026-08-06T04:56:19.555Z")!! + val winterUtc = parseIsoToEpochMillisOrNull("2026-01-15T04:56:19.555Z")!! + + assertThat(summerUtc - summer).isEqualTo(2 * 3_600_000L) // CEST + assertThat(winterUtc - winter).isEqualTo(1 * 3_600_000L) // CET + } + + @Test fun `garbage returns null rather than a wrong instant`() { + listOf("", " ", "not a date", "2026-13-45T99:99:99Z", "Z", "+02:00").forEach { text -> + assertWithMessage("input %s", text) + .that(parseIsoToEpochMillisOrNull(text)).isNull() + } + } +} diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 39dfbced474b..6cf027dc73dc 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { api(libs.org.apache.commons.lang3) api(libs.net.danlew.android.joda) + api(libs.kotlinx.datetime) //RxBus / RxJava base api(libs.io.reactivex.rxjava3.rxkotlin) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt index 4633660cc91a..eb133405827a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt @@ -7,13 +7,14 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import app.aaps.core.data.datetime.parseIsoToEpochMillisOrNull +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format +import kotlinx.datetime.format.char +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.Json -import org.joda.time.DateTime -import org.joda.time.format.ISODateTimeFormat -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone +import kotlin.time.Instant @Serializable data class RT( @@ -76,19 +77,40 @@ data class RT( return fromISODateString(decoder.decodeString()) } - fun fromISODateString(isoDateString: String): Long { - val parser = ISODateTimeFormat.dateTimeParser() - val dateTime = DateTime.parse(isoDateString, parser) - return dateTime.toDate().time + /** + * Was joda's `ISODateTimeFormat.dateTimeParser()`. The replacement accepts the same shapes - + * this parses device status written by other Nightscout uploaders, not only what AAPS wrote, + * so `+0200` without a colon and offset-less values have to keep working. + * + * It still throws on input it cannot read, as joda did. A silent epoch-0 timestamp inside an + * APS result would be worse than a loud failure. + */ + fun fromISODateString(isoDateString: String): Long = + parseIsoToEpochMillisOrNull(isoDateString) + ?: throw IllegalArgumentException("Invalid format: $isoDateString") + + /** + * `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` in UTC, always with three fractional digits. + * + * Built explicitly rather than with `Instant.toString()`, which drops trailing zeros and + * omits the fraction altogether on a whole second. + */ + private val isoOut = LocalDateTime.Format { + year(); char('-'); monthNumber(); char('-'); day() + char('T') + hour(); char(':'); minute(); char(':'); second() + char('.'); secondFraction(3) } - fun toISOString(date: Long): String { - @Suppress("SpellCheckingInspection", "LocalVariableName") - val FORMAT_DATE_ISO_OUT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - val f: DateFormat = SimpleDateFormat(FORMAT_DATE_ISO_OUT, Locale.getDefault()) - f.timeZone = TimeZone.getTimeZone("UTC") - return f.format(date) - } + /** + * This used to be `SimpleDateFormat(pattern, Locale.getDefault())`, which resolves its + * calendar from the locale. On a phone set to Thai that selects the Buddhist calendar and + * the year was written as 2569 instead of 2026 - a wire timestamp 543 years in the future. + * The formatter here is locale independent, which fixes that as well as removing the JVM + * dependency. `RtIsoStringParityTest` pins both halves. + */ + fun toISOString(date: Long): String = + isoOut.format(Instant.fromEpochMilliseconds(date).toLocalDateTime(TimeZone.UTC)) + "Z" } companion object { diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt index b4b5c3c7a515..d19bb38b0f66 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt @@ -1,28 +1,39 @@ package app.aaps.core.interfaces.utils import androidx.annotation.VisibleForTesting -import androidx.collection.LongSparseArray -import java.time.Instant -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.ZoneId +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.minus +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.Instant object MidnightTime { + /** + * Was an `androidx.collection.LongSparseArray`, which is Android only. A plain map keeps the + * behaviour: the entries are only ever looked up by key, never iterated, so the ordering + * difference between the two does not reach any caller. + * + * Note this cache has never actually filled - see [calc]. + */ @VisibleForTesting - val times = LongSparseArray() + val times = HashMap() private const val THRESHOLD = 100000 + private fun zone() = TimeZone.currentSystemDefault() + /** * Epoch time of last midnight * * @return epoch millis */ - fun calc(): Long = - LocalDateTime.now().atZone(ZoneId.systemDefault()) - .with(LocalTime.of(0, 0, 0, 0)) - .toInstant().toEpochMilli() + fun calc(): Long = calc(Clock.System.now().toEpochMilliseconds()) /** * Today's time with 'minutes' from midnight @@ -33,9 +44,9 @@ object MidnightTime { fun calcMidnightPlusMinutes(minutes: Int): Long { val h = (minutes / 60) % 24 val m = minutes % 60 - return LocalDateTime.now().atZone(ZoneId.systemDefault()) - .with(LocalTime.of(h, m, 0, 0)) - .toInstant().toEpochMilli() + val tz = zone() + val date = Clock.System.now().toLocalDateTime(tz).date + return LocalDateTime(date, LocalTime(h, m)).toInstant(tz).toEpochMilliseconds() } /** @@ -46,25 +57,29 @@ object MidnightTime { */ fun calc(time: Long): Long { synchronized(times) { - val m = times[time] ?: Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()) - .with(LocalTime.of(0, 0, 0, 0)) - .toInstant().toEpochMilli() - if (times.size() > THRESHOLD) resetCache() + // Nothing is ever put into `times`, so this lookup always misses and the value is always + // recomputed. That is how it behaved with the LongSparseArray too - the `put` has been + // missing since well before the multiplatform work - so it is kept as is rather than + // "fixed" here: adding the write back would change memory use on a hot path (the graph + // renderer calls this per point) and belongs in its own change. + val m = times[time] ?: midnightOf(time) + if (times.size > THRESHOLD) resetCache() return m } } + private fun midnightOf(time: Long): Long { + val tz = zone() + return Instant.fromEpochMilliseconds(time).toLocalDateTime(tz).date.atStartOfDayIn(tz).toEpochMilliseconds() + } + /** * Epoch time of last midnight 'days' back * * @param daysBack how many days back * @return epoch millis of midnight */ - fun calcDaysBack(daysBack: Long): Long = - LocalDateTime.now().atZone(ZoneId.systemDefault()) - .with(LocalTime.of(0, 0, 0, 0)) - .minusDays(daysBack) - .toInstant().toEpochMilli() + fun calcDaysBack(daysBack: Long): Long = calcDaysBack(Clock.System.now().toEpochMilliseconds(), daysBack) /** * Epoch time of last midnight 'days' back from time @@ -73,14 +88,17 @@ object MidnightTime { * @param daysBack how many days back * @return epoch millis of midnight */ - fun calcDaysBack(time: Long, daysBack: Long): Long = - Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()) - .with(LocalTime.of(0, 0, 0, 0)) - .minusDays(daysBack) - .toInstant().toEpochMilli() + fun calcDaysBack(time: Long, daysBack: Long): Long { + val tz = zone() + // Subtract whole calendar days on the DATE, then take the start of that day, so a daylight + // saving change inside the range shifts the result by the offset rather than by a fixed + // number of milliseconds. + val date = Instant.fromEpochMilliseconds(time).toLocalDateTime(tz).date.minus(daysBack.toInt(), DateTimeUnit.DAY) + return date.atStartOfDayIn(tz).toEpochMilliseconds() + } @VisibleForTesting fun resetCache() { times.clear() } -} \ No newline at end of file +} diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt new file mode 100644 index 000000000000..c813ef18108a --- /dev/null +++ b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt @@ -0,0 +1,114 @@ +package app.aaps.core.interfaces.aps + +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +/** + * Pins [RT.TimestampToIsoSerializer.toISOString] against the `SimpleDateFormat` implementation it is + * being converted away from. + * + * This string is not cosmetic: it is what the APS result timestamp is serialized to, so it travels + * into stored results and to Nightscout. The format has to stay `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` in + * UTC, with exactly three fractional digits, including when the fraction is zero - which rules out + * `Instant.toString()`, since that drops trailing zeros and omits the fraction entirely on a whole + * second. + */ +class RtIsoStringParityTest { + + private lateinit var originalLocale: Locale + + @BeforeEach fun save() { + originalLocale = Locale.getDefault() + } + + @AfterEach fun restore() { + Locale.setDefault(originalLocale) + } + + /** A literal copy of the original body, so the comparison is against real old behaviour. */ + private fun reference(date: Long, locale: Locale): String { + val f: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", locale) + f.timeZone = TimeZone.getTimeZone("UTC") + return f.format(date) + } + + private val samples = listOf( + 0L, // epoch + 1_785_992_181_588L, // arbitrary, non zero millis + 1_785_992_181_000L, // whole second - the case Instant.toString() would render differently + 1_785_992_181_007L, // sub-10 millis, needs zero padding + 1_767_225_600_000L, // 2026-01-01T00:00:00Z + 253_402_300_799_000L // year 9999 + ) + + @Test fun `matches SimpleDateFormat in a neutral locale`() { + Locale.setDefault(Locale.US) + samples.forEach { t -> + assertWithMessage("t=%s", t) + .that(RT.TimestampToIsoSerializer.toISOString(t)) + .isEqualTo(reference(t, Locale.US)) + } + } + + @Test fun `always has exactly three fractional digits`() { + Locale.setDefault(Locale.US) + samples.forEach { t -> + assertWithMessage("t=%s", t) + .that(RT.TimestampToIsoSerializer.toISOString(t)) + .matches("""\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z""") + } + } + + @Test fun `round trips through the parser`() { + Locale.setDefault(Locale.US) + samples.forEach { t -> + val text = RT.TimestampToIsoSerializer.toISOString(t) + assertWithMessage("t=%s via %s", t, text) + .that(RT.TimestampToIsoSerializer.fromISODateString(text)).isEqualTo(t) + } + } + + /** + * The reason this conversion is worth doing beyond portability. + * + * `SimpleDateFormat` resolves its calendar from the locale, and a Thai locale selects the + * Buddhist calendar - so the old code rendered the year as 2569 rather than 2026 for a user + * whose phone is set to Thai. That is a wire format bug, not a display preference: the receiver + * gets a timestamp 543 years in the future. + * + * This asserts the defect exists in the OLD implementation, so the claim is measured rather than + * assumed, and so the test fails loudly if a JDK change ever makes it untrue. + */ + @Test fun `the SimpleDateFormat version was locale sensitive`() { + val thai = Locale.forLanguageTag("th-TH-u-ca-buddhist") + val inThai = reference(1_785_992_181_588L, thai) + val inUs = reference(1_785_992_181_588L, Locale.US) + + assertWithMessage("old implementation in Thai locale: %s", inThai) + .that(inThai).isNotEqualTo(inUs) + assertThat(inUs).startsWith("2026-") + assertThat(inThai).startsWith("2569-") + } + + /** ...and the replacement must not be, whatever the phone's locale is set to. */ + @Test fun `the new version is locale independent`() { + val expected = RT.TimestampToIsoSerializer.toISOString(1_785_992_181_588L) + listOf( + Locale.forLanguageTag("th-TH-u-ca-buddhist"), + Locale.forLanguageTag("ar-EG-u-nu-arab"), + Locale.forLanguageTag("hi-IN"), + Locale.forLanguageTag("ja-JP-u-ca-japanese") + ).forEach { locale -> + Locale.setDefault(locale) + assertWithMessage("locale %s", locale) + .that(RT.TimestampToIsoSerializer.toISOString(1_785_992_181_588L)).isEqualTo(expected) + } + } +} diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeParityTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeParityTest.kt new file mode 100644 index 000000000000..4b0a7deb559e --- /dev/null +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeParityTest.kt @@ -0,0 +1,130 @@ +package app.aaps.core.objects.interfaces.utils + +import app.aaps.core.interfaces.utils.MidnightTime +import com.google.common.truth.Truth.assertWithMessage +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.LocalTime +import java.time.ZoneId +import java.util.TimeZone + +/** + * Pins [MidnightTime] against the `java.time` implementation it is being converted away from. + * + * The reference below is a literal copy of the original bodies. Every assertion compares the live + * [MidnightTime] to it, so the conversion to kotlinx-datetime has to reproduce the old answer + * exactly rather than merely produce a plausible one. + * + * **Why this file exists rather than trusting the existing [MidnightTimeTest].** That suite only + * asks "is the result midnight, today, in this machine's zone". Every implementation that is roughly + * right passes it. The interesting cases are the two hours a year when local midnight is not a + * simple offset from UTC, and this repository has already shipped a fix for exactly that + * (`calculated day's difference over DST correctly`). So the sweep below runs hour by hour across + * both transitions. + * + * The zone is pinned to Europe/Prague for the same reason the iOS workflow pins it: a CI machine is + * usually UTC, where every one of these assertions holds trivially because the offset never moves. + */ +class MidnightTimeParityTest { + + private lateinit var original: TimeZone + + @BeforeEach fun pinZone() { + original = TimeZone.getDefault() + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague")) + MidnightTime.resetCache() + } + + @AfterEach fun restoreZone() { + TimeZone.setDefault(original) + MidnightTime.resetCache() + } + + // ------------------------------------------------------------------ the reference + + private fun referenceCalc(time: Long): Long = + Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()) + .with(LocalTime.of(0, 0, 0, 0)) + .toInstant().toEpochMilli() + + private fun referenceCalcDaysBack(time: Long, daysBack: Long): Long = + Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()) + .with(LocalTime.of(0, 0, 0, 0)) + .minusDays(daysBack) + .toInstant().toEpochMilli() + + // ------------------------------------------------------------------ sweeps + + /** 2026-03-29 in Europe/Prague: 02:00 CET jumps straight to 03:00 CEST, so 02:xx never happens. */ + private val springForward = Instant.parse("2026-03-28T00:00:00Z").toEpochMilli() + + /** 2026-10-25 in Europe/Prague: 03:00 CEST falls back to 02:00 CET, so 02:xx happens twice. */ + private val fallBack = Instant.parse("2026-10-24T00:00:00Z").toEpochMilli() + + private val hour = 3_600_000L + + private fun sweep(from: Long, hours: Int, label: String) { + for (i in 0 until hours) { + val t = from + i * hour + assertWithMessage("%s: calc(%s)", label, Instant.ofEpochMilli(t)) + .that(MidnightTime.calc(t)).isEqualTo(referenceCalc(t)) + } + } + + @Test fun `calc matches java time across spring forward`() = sweep(springForward, 72, "spring") + + @Test fun `calc matches java time across fall back`() = sweep(fallBack, 72, "fall") + + @Test fun `calcDaysBack matches java time across spring forward`() { + for (i in 0 until 72) { + val t = springForward + i * hour + for (daysBack in 0L..7L) + assertWithMessage("spring: calcDaysBack(%s, %s)", Instant.ofEpochMilli(t), daysBack) + .that(MidnightTime.calcDaysBack(t, daysBack)) + .isEqualTo(referenceCalcDaysBack(t, daysBack)) + } + } + + @Test fun `calcDaysBack matches java time across fall back`() { + for (i in 0 until 72) { + val t = fallBack + i * hour + for (daysBack in 0L..7L) + assertWithMessage("fall: calcDaysBack(%s, %s)", Instant.ofEpochMilli(t), daysBack) + .that(MidnightTime.calcDaysBack(t, daysBack)) + .isEqualTo(referenceCalcDaysBack(t, daysBack)) + } + } + + /** + * Proves the sweeps above are not vacuous. + * + * If the default zone failed to apply, or if a zone without daylight saving were chosen, every + * midnight would sit at the same offset from UTC and the parity assertions would hold for any + * implementation that simply floored to a whole day. This asserts the opposite: across the + * spring transition the local midnights really do land on two different UTC offsets, so the + * sweep is exercising the case it claims to. + */ + @Test fun `the sweep actually crosses an offset change`() { + val offsets = (0 until 72) + .map { springForward + it * hour } + .map { t -> Math.floorMod(MidnightTime.calc(t), 24 * hour) } + .toSet() + assertWithMessage("local midnight must sit at more than one UTC offset across a DST change") + .that(offsets.size).isGreaterThan(1) + } + + /** + * A whole ordinary year, one sample a day, so the sweeps above are not the only evidence and a + * mistake that only shows outside a transition week still fails. + */ + @Test fun `calc matches java time over a full year`() { + val start = Instant.parse("2026-01-01T12:00:00Z").toEpochMilli() + for (day in 0 until 365) { + val t = start + day * 24 * hour + assertWithMessage("day %s (%s)", day, Instant.ofEpochMilli(t)) + .that(MidnightTime.calc(t)).isEqualTo(referenceCalc(t)) + } + } +} diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeTest.kt index 06deec9fd747..5135c4e12034 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/MidnightTimeTest.kt @@ -61,6 +61,8 @@ class MidnightTimeTest { val now = System.currentTimeMillis() MidnightTime.calc(now) MidnightTime.resetCache() - assertThat(MidnightTime.times.size().toLong()).isEqualTo(0L) + // Passes trivially: `calc` never writes to `times`, so the map is already empty before the + // reset. Kept as a regression guard for the day the cache is actually wired up. + assertThat(MidnightTime.times.size.toLong()).isEqualTo(0L) } } From f27e8b50aec55484c98a22654d4cc2505306e167 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 22:36:34 +0200 Subject: [PATCH 044/146] Update KMP doc --- _docs/KMP_IOS_FEASIBILITY.md | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 818b6afc8dbc..9dcef139e27d 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -1652,6 +1652,101 @@ The general form of the question was already answered in public - coil-kt/coil a both ship these four plugins on Kotlin 2.4.10 + AGP 9.3.1 + CMP 1.11.1 - so the spike only ever checked that nothing repo-specific interferes. Nothing did. +### Wave 18 - `:core:interfaces` starts moving, and three bugs fall out (done) + +Commits `7ba30dcdc6`, `8e928f3036`, `ba3c465093`. + +**The strategy was wrong at first, and the correction matters.** `:core:interfaces` was measured the +same way `:core:ui` was - "has an Android import, therefore blocked" - which is a reasonable question +for a UI module and the wrong one for a module of *interfaces*. An Android type in a signature there +is usually a leak in the contract, not a property of the concept. The split is three ways: + +1. **direct elimination** - a multiplatform equivalent exists +2. **abstraction + platform implementation** - the concept is real everywhere, only the type is + Android's; narrow the contract and let the Android implementation keep the Android type +3. **genuinely platform-only** - pump BLE, wear tiles, notification channels; stays in `androidMain` + +#### `androidx` is not uniformly Android, and guessing it is got this wrong twice + +Probed against a real `compileKotlinIosArm64`, not assumed: + +| artifact | verdict | +|---|---| +| `androidx.collection` | **multiplatform** since 1.4.0 - `LongSparseArray` compiles for iOS unchanged | +| `androidx.compose.*` | **free** - Compose Multiplatform publishes the same package names | +| `androidx.annotation` | **split** - `@VisibleForTesting` is common, `@StringRes`/`@RawRes` are not | +| `appcompat`, `fragment`, `activity`, `documentfile` | Android only | + +That correction alone moved 17 files out of the blocked set. `LongSparseArray` had been written down +as "replace with a plain `Map`" - which would have been a **bug**, not just wasted work: it iterates +in ascending key order, and the sensitivity/autosens plugins and the TDD/TIR screens walk it by +index. A `HashMap` would have silently reordered them. + +Corrected count: **64 directly blocked, 85 after propagation, 168 clean** of 253. + +#### org.json, and why the oracle version decides what "correct" means + +`OrgJsonCompat` moved from `:plugins:sync` to `:core:data` so the profile spine can reach it. Its +first parity run failed, and the reason is worth keeping: **Android's `org.json` and Maven's +`org.json:json` are different implementations.** `optString` of a JSON null gives `"null"` on Android +and `""` in Crockford's. The right oracle is `com.vaadin.external.google:android-json`, which is +AOSP's own implementation repackaged for the JVM - it is what `jsonassert` already pulled in, so the +existing Android module tests were measured against it too. It is now declared explicitly. + +Two more findings from that suite: + +- **`optInt` was wrong.** `org.json` reads a numeric **string** through `Double.intValue()`, which + *saturates*, but a real JSON **number** through `Long.intValue()`, which *truncates*. Reading both + through `Long.toInt()` looks obviously right and turns `"1785992181588"` into **-714213548**. +- **Reading is safe to shim; writing is not.** The old suite only covered reads. `org.json` writes a + whole-numbered double without its fraction (`1.0` -> `1`), rejects non-finite doubles that kotlinx + happily emits as invalid JSON, and accepts malformed input kotlinx rejects. Since + `Profile.toPureNsJson` feeds Nightscout, those change bytes on the wire. They are asserted as + divergences rather than hidden. + +**The org.json call sites were deliberately NOT converted.** They are viral: `Profile.toPureNsJson()`, +`ProfileStore.with()`, `SingleProfile.ic/isf/basal` and `APSResult.json()` carry `JSONObject` and +`JSONArray` in their **contracts**, and 164 files touch `org.json`. That is its own deliberate piece, +starting from characterization tests over real profile documents. + +#### A locale bug on the wire + +`RT.toISOString` built the APS result timestamp with `SimpleDateFormat(pattern, Locale.getDefault())`. +`SimpleDateFormat` resolves its calendar from the locale, and a Thai locale selects the Buddhist +calendar: the year went out as **2569 instead of 2026**, a timestamp 543 years in the future, for any +user whose phone is set to Thai. Pre-existing on `dev`, fixed by the conversion because the kotlinx +formatter is locale independent. `RtIsoStringParityTest` asserts the defect existed in the old code +too, so the claim is measured rather than asserted. + +The formatter is built explicitly instead of using `Instant.toString()`, which drops trailing zeros +and omits the fraction on a whole second - the format has to stay `.SSS` always. + +#### joda is gone from `:core:interfaces` + +Only `RT.kt` used it. The parser matters more than it looks: `RT.deserialize` runs on **device status +written by other Nightscout uploaders**, so `+0200` without a colon, offset-less values and bare dates +all arrive in practice, and kotlinx rejects all three. `:core:nssdk` already had a proven lenient +parser, so that logic moved to `:core:data` and is pinned against real joda over 15 shapes. + +Two judgement calls, both deliberate: + +- it is **knowingly duplicated** in `:core:nssdk`, which has *zero* project dependencies; coupling a + standalone SDK module to the app's data module to share ~30 lines is the worse trade +- joda **threw** on garbage while the nssdk copy returns `0L`, so the shared function returns `Long?` + and each caller keeps its own behaviour - `RT` throws, nssdk maps to `0L` + +#### Also found, deliberately not fixed + +`MidnightTime.calc(time)` reads its cache, computes on a miss, and **never stores the result**. The +map is always empty, the `THRESHOLD` reset is dead, and the existing `resetCache` test passes +trivially. It predates the multiplatform work. Adding the write back changes memory use on a hot path +(the graph renderer calls this per point), so it is left alone and recorded here instead. + +The DST sweeps written for that file are the useful part: the old test only asked "is this midnight, +today, in this zone", which any roughly-right implementation passes. It now runs hour by hour across +both 2026 transitions in Europe/Prague, with a guard test asserting the sweep really crosses an offset +change - otherwise a UTC CI machine makes the whole thing vacuous, the same trap as the iOS workflow. + --- ## 9. Open decisions From 8bca33711293f5dd7496cf44f3354870b5fc4c0f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 22:42:09 +0200 Subject: [PATCH 045/146] :core:interfaces drop vestigial Parcelable --- core/interfaces/build.gradle.kts | 1 - .../app/aaps/core/interfaces/maintenance/PrefMetadata.kt | 6 +----- .../app/aaps/core/interfaces/maintenance/PrefsFile.kt | 9 ++------- .../app/aaps/core/interfaces/maintenance/PrefsStatus.kt | 5 ++--- implementation/build.gradle.kts | 1 - .../aaps/implementation/maintenance/data/PrefsFormat.kt | 4 ---- 6 files changed, 5 insertions(+), 21 deletions(-) diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 6cf027dc73dc..6cc7889b2f2a 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -4,7 +4,6 @@ import kotlin.math.min plugins { alias(libs.plugins.android.library) alias(libs.plugins.compose.compiler) - id("kotlin-parcelize") id("kotlinx-serialization") id("android-module-dependencies") id("test-module-dependencies") diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt index 747a5c7eab66..723e9e2c48ef 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt @@ -1,7 +1,3 @@ package app.aaps.core.interfaces.maintenance -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) : Parcelable +data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt index 98c92c4ffa4c..999cc0b7cf67 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt @@ -1,18 +1,13 @@ package app.aaps.core.interfaces.maintenance -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue - -@Parcelize data class PrefsFile( val name: String, val content: String, // metadata here is used only for list display - val metadata: @RawValue Map, + val metadata: Map, // Stable unique identifier from the storage provider (e.g. Google Drive file id). // Null for local files, which are uniquely identified by their name. val id: String? = null -) : Parcelable \ No newline at end of file +) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt index aaf238d8f96f..a5e2f526fc2c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt @@ -1,12 +1,11 @@ package app.aaps.core.interfaces.maintenance -import android.os.Parcelable import androidx.compose.ui.graphics.vector.ImageVector -interface PrefsStatus : Parcelable { +interface PrefsStatus { val icon: ImageVector val isOk: Boolean get() = false val isWarning: Boolean get() = false val isError: Boolean get() = false -} \ No newline at end of file +} diff --git a/implementation/build.gradle.kts b/implementation/build.gradle.kts index 0118259c35a4..66b5d292dbf0 100644 --- a/implementation/build.gradle.kts +++ b/implementation/build.gradle.kts @@ -2,7 +2,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.ksp) alias(libs.plugins.compose.compiler) - id("kotlin-parcelize") id("android-module-dependencies") id("all-open-dependencies") id("test-module-dependencies") diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/data/PrefsFormat.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/data/PrefsFormat.kt index 07b6c9b173ee..27facec640e9 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/data/PrefsFormat.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/data/PrefsFormat.kt @@ -9,8 +9,6 @@ import androidx.documentfile.provider.DocumentFile import app.aaps.core.interfaces.maintenance.PrefMetadataMap import app.aaps.core.interfaces.maintenance.Prefs import app.aaps.core.interfaces.maintenance.PrefsStatus -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize interface PrefsFormat { companion object { @@ -24,12 +22,10 @@ interface PrefsFormat { fun isPreferencesFile(file: DocumentFile, preloadedContents: String? = null): Boolean } -@Parcelize enum class PrefsStatusImpl : PrefsStatus { OK, WARN, ERROR, UNKNOWN, DISABLED; - @IgnoredOnParcel override val icon: ImageVector get() = when (this) { OK -> Icons.Default.Check From 8e624a5cbdf2a2fc0eb00ffd76ff4d2625e7c46a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 22:50:37 +0200 Subject: [PATCH 046/146] Remove dead View based stats code --- .../aaps/core/interfaces/stats/DexcomTIR.kt | 53 --------- .../app/aaps/core/interfaces/stats/TIR.kt | 8 -- .../implementation/stats/DexcomTirImpl.kt | 106 +----------------- .../app/aaps/implementation/stats/TirImpl.kt | 73 ------------ .../stats/TotalDailyDoseExtension.kt | 95 ---------------- .../implementation/stats/DexcomTirImplTest.kt | 28 ----- .../implementation/stats/TirImplViewTest.kt | 45 -------- 7 files changed, 1 insertion(+), 407 deletions(-) delete mode 100644 implementation/src/main/kotlin/app/aaps/implementation/stats/TotalDailyDoseExtension.kt delete mode 100644 implementation/src/test/kotlin/app/aaps/implementation/stats/TirImplViewTest.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt index e6fdef1587d5..386f6f38c2fe 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt @@ -1,10 +1,5 @@ package app.aaps.core.interfaces.stats -import android.content.Context -import android.widget.TableRow -import android.widget.TextView -import app.aaps.core.interfaces.profile.ProfileUtil - /** * Interface for Dexcom-style Time In Range (TIR) statistics. * @@ -37,54 +32,6 @@ interface DexcomTIR { */ fun calculateSD(): Double - /** - * Creates an Android TextView displaying the estimated HbA1c. - * - * HbA1c is calculated from mean glucose using the formula: - * HbA1c (%) = (mean + 46.7) / 28.7 - * HbA1c (mmol/mol) = ((mean + 46.7) / 28.7 - 2.15) * 10.929 - * - * @param context Android context for creating the TextView - * @return TextView with formatted HbA1c value, or empty if no data - */ - fun toHbA1cView(context: Context): TextView - - /** - * Creates an Android TextView displaying the standard deviation. - * - * @param context Android context for creating the TextView - * @param profileUtil Utility for converting glucose values to user's preferred units - * @return TextView with formatted standard deviation value - */ - fun toSDView(context: Context, profileUtil: ProfileUtil): TextView - - /** - * Creates an Android TextView displaying the range headers with threshold values. - * - * Shows the 5 glucose ranges with their thresholds for both day and night periods. - * - * @param context Android context for creating the TextView - * @param profileUtil Utility for converting glucose values to user's preferred units - * @return TextView with formatted range headers - */ - fun toRangeHeaderView(context: Context, profileUtil: ProfileUtil): TextView - - /** - * Creates an Android TableRow with column headers for the TIR table. - * - * @param context Android context for creating the TableRow - * @return TableRow with headers: Very Low, Low, In Range, High, Very High - */ - fun toTableRowHeader(context: Context): TableRow - - /** - * Creates an Android TableRow with percentage values for each glucose range. - * - * @param context Android context for creating the TableRow - * @return TableRow with formatted percentage values for each range - */ - fun toTableRow(context: Context): TableRow - // Data accessors for Compose /** diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt index b2a7776d33d9..5a1a8e8deb2a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt @@ -1,10 +1,5 @@ package app.aaps.core.interfaces.stats -import android.content.Context -import android.widget.TableRow -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DateUtil - interface TIR { val date: Long @@ -19,7 +14,4 @@ interface TIR { fun below() fun inRange() fun above() - - fun toTableRow(context: Context, rh: ResourceHelper, dateUtil: DateUtil): TableRow - fun toTableRow(context: Context, rh: ResourceHelper, days: Int): TableRow } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/stats/DexcomTirImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/stats/DexcomTirImpl.kt index 91c106a53323..bc3ad50ab7a6 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/stats/DexcomTirImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/stats/DexcomTirImpl.kt @@ -1,15 +1,7 @@ package app.aaps.implementation.stats -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Typeface -import android.view.Gravity -import android.widget.TableRow -import android.widget.TextView import app.aaps.core.data.configuration.Constants -import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.stats.DexcomTIR -import app.aaps.core.ui.R import dagger.Reusable import java.util.Calendar import kotlin.math.pow @@ -27,7 +19,7 @@ import kotlin.math.sqrt * - Time-of-day aware range thresholds (different high threshold for day vs night) * - Statistical calculations (mean, standard deviation) * - HbA1c estimation from mean glucose - * - Both Android View-based and Compose-compatible accessors + * - Compose-compatible accessors * * Range thresholds (from Constants): * - Very Low: < 54 mg/dL (3.0 mmol/L) @@ -170,100 +162,4 @@ class DexcomTirImpl : DexcomTIR { return sqrt(standardDeviation / count) } - override fun toHbA1cView(context: Context): TextView = - TextView(context).apply { - text = - if (count == 0) "" - else context.getString(R.string.hba1c) + " " + - (10 * (mean() + 46.7) / 28.7).roundToInt() / 10.0 + "%" + - " (" + - (((mean() + 46.7) / 28.7 - 2.15) * 10.929).roundToInt() + - " mmol/mol)" - setTypeface(typeface, Typeface.NORMAL) - gravity = Gravity.CENTER_HORIZONTAL - } - - @SuppressLint("SetTextI18n") - override fun toSDView(context: Context, profileUtil: ProfileUtil): TextView = - TextView(context).apply { - val sd = calculateSD() - text = "\n" + context.getString(R.string.std_deviation, profileUtil.fromMgdlToStringInUnits(sd)) - setTypeface(typeface, Typeface.NORMAL) - gravity = Gravity.CENTER_HORIZONTAL - } - - override fun toRangeHeaderView(context: Context, profileUtil: ProfileUtil): TextView = - TextView(context).apply { - text = StringBuilder() - .append(context.getString(R.string.detailed_14_days)) - .append("\n") - .append(context.getString(R.string.day_tir)) - .append(" (") - .append(profileUtil.fromMgdlToStringInUnits(0.0)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(veryLowTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(lowTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(highTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(veryHighTirMgdl)) - .append("-∞)\n") - .append(context.getString(R.string.night_tir)) - .append(" (") - .append(profileUtil.fromMgdlToStringInUnits(0.0)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(veryLowTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(lowTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(highNightTirMgdl)) - .append("-") - .append(profileUtil.stringInCurrentUnitsDetect(veryHighTirMgdl)) - .append("-∞)\n") - .toString() - setTypeface(typeface, Typeface.BOLD) - gravity = Gravity.CENTER_HORIZONTAL - setTextAppearance(android.R.style.TextAppearance_Material_Medium) - } - - override fun toTableRowHeader(context: Context): TableRow = - TableRow(context).also { header -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT) - header.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - header.gravity = Gravity.CENTER_HORIZONTAL - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0; weight = 1f }; text = context.getString(R.string.veryLow) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1; weight = 1f }; text = context.getString(R.string.low) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2; weight = 1f }; text = context.getString(R.string.in_range) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3; weight = 1f }; text = context.getString(R.string.high) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 4; weight = 1f }; text = context.getString(R.string.veryHigh) }) - } - - @SuppressLint("SetTextI18n") - override fun toTableRow(context: Context): TableRow = - TableRow(context).also { row -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 1f) - row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - row.gravity = Gravity.CENTER_HORIZONTAL - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0 }; text = - context.getString(R.string.formatPercent, veryLowPct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1 }; text = - context.getString(R.string.formatPercent, lowPct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2 }; text = - context.getString(R.string.formatPercent, inRangePct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3 }; text = - context.getString(R.string.formatPercent, highPct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 4 }; text = - context.getString(R.string.formatPercent, veryHighPct()) - }) - } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/stats/TirImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/stats/TirImpl.kt index c60962eccf84..e9711caf24ff 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/stats/TirImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/stats/TirImpl.kt @@ -1,14 +1,6 @@ package app.aaps.implementation.stats -import android.annotation.SuppressLint -import android.content.Context -import android.view.Gravity -import android.widget.TableRow -import android.widget.TextView -import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.stats.TIR -import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.ui.R class TirImpl(override val date: Long, override val lowThreshold: Double, override val highThreshold: Double) : TIR { @@ -33,69 +25,4 @@ class TirImpl(override val date: Long, override val lowThreshold: Double, overri override fun above() { above++; count++ } - - private fun belowPct() = if (count > 0) below.toDouble() / count * 100.0 else 0.0 - private fun inRangePct() = if (count > 0) 100 - belowPct() - abovePct() else 0.0 - private fun abovePct() = if (count > 0) above.toDouble() / count * 100.0 else 0.0 - - companion object { - - fun toTableRowHeader(context: Context, rh: ResourceHelper): TableRow = - TableRow(context).also { header -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT) - header.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - header.gravity = Gravity.CENTER_HORIZONTAL - header.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0; weight = 1f }; text = - rh.gs(app.aaps.core.ui.R.string.date) - }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1; weight = 1f }; text = rh.gs(R.string.below) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2; weight = 1f }; text = rh.gs(R.string.in_range) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3; weight = 1f }; text = rh.gs(R.string.above) }) - } - } - - override fun toTableRow(context: Context, rh: ResourceHelper, dateUtil: DateUtil): TableRow = - TableRow(context).also { row -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 1f) - row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - row.gravity = Gravity.CENTER_HORIZONTAL - row.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0 }; text = dateUtil.dateStringShort(date) }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1 }; text = - rh.gs(R.string.formatPercent, belowPct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2 }; text = - rh.gs(R.string.formatPercent, inRangePct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3 }; text = - rh.gs(R.string.formatPercent, abovePct()) - }) - } - - @SuppressLint("SetTextI18n") - override fun toTableRow(context: Context, rh: ResourceHelper, days: Int): TableRow = - TableRow(context).also { row -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 1f) - row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - row.gravity = Gravity.CENTER_HORIZONTAL - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0 }; text = - "%02d".format(days) + " " + rh.gs(app.aaps.core.interfaces.R.string.days) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1 }; text = - rh.gs(R.string.formatPercent, belowPct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2 }; text = - rh.gs(R.string.formatPercent, inRangePct()) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3 }; text = - rh.gs(R.string.formatPercent, abovePct()) - }) - } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/stats/TotalDailyDoseExtension.kt b/implementation/src/main/kotlin/app/aaps/implementation/stats/TotalDailyDoseExtension.kt deleted file mode 100644 index 7a81516212bc..000000000000 --- a/implementation/src/main/kotlin/app/aaps/implementation/stats/TotalDailyDoseExtension.kt +++ /dev/null @@ -1,95 +0,0 @@ -package app.aaps.implementation.stats - -import android.annotation.SuppressLint -import android.content.Context -import android.view.Gravity -import android.widget.TableRow -import android.widget.TextView -import app.aaps.core.data.model.TDD -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.implementation.R - -val TDD.total - get() = if (totalAmount > 0) totalAmount else basalAmount + bolusAmount - -val TDD.basalPct: Double - get() = if (total > 0) basalAmount / total * 100 else 0.0 - -fun TDD.Companion.toTableRowHeader(context: Context, rh: ResourceHelper, includeCarbs: Boolean): TableRow = - TableRow(context).also { header -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT) - header.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - header.gravity = Gravity.CENTER_HORIZONTAL - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0; weight = 1f }; text = rh.gs(app.aaps.core.ui.R.string.date) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1; weight = 1f }; text = "∑" }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2; weight = 1f }; text = rh.gs(app.aaps.core.ui.R.string.bolus) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3; weight = 1f }; text = rh.gs(app.aaps.core.ui.R.string.basal) }) - header.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 4; weight = 1f }; text = rh.gs(app.aaps.core.ui.R.string.basalpct) }) - if (includeCarbs) - header.addView(TextView(context).apply { layoutParams = lp.apply { column = 5; weight = 1f }; text = rh.gs(R.string.carbs_short) }) - } - -fun TDD.toTableRow(context: Context, rh: ResourceHelper, dateUtil: DateUtil, includeCarbs: Boolean): TableRow = - TableRow(context).also { row -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f } - if ((total.isNaN() || bolusAmount.isNaN() || basalAmount.isNaN() || carbs.isNaN()).not()) { - row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - row.gravity = Gravity.CENTER_HORIZONTAL - row.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0 }; text = dateUtil.dateStringShort(timestamp) }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, total) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, bolusAmount) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, basalAmount) - }) - row.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 4 }; text = rh.gs(app.aaps.core.ui.R.string.formatPercent, basalPct) }) - if (includeCarbs) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 5 }; text = rh.gs( - app.aaps.core.ui.R.string.format_carbs, carbs - .toInt() - ) - }) - } - } - -@SuppressLint("SetTextI18n") -fun TDD.toTableRow(context: Context, rh: ResourceHelper, days: Int, includeCarbs: Boolean): TableRow = - TableRow(context).also { row -> - val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f } - if ((total.isNaN() || bolusAmount.isNaN() || basalAmount.isNaN() || carbs.isNaN()).not()) { - row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT) - row.gravity = Gravity.CENTER_HORIZONTAL - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 0 }; text = - "%02d".format(days) + " " + rh.gs(app.aaps.core.interfaces.R.string.days) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 1 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, total) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 2 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, bolusAmount) - }) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 3 }; text = - rh.gs(app.aaps.core.ui.R.string.format_insulin_units1, basalAmount) - }) - row.addView(TextView(context).apply { gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 4 }; text = rh.gs(app.aaps.core.ui.R.string.formatPercent, basalPct) }) - if (includeCarbs) - row.addView(TextView(context).apply { - gravity = Gravity.CENTER_HORIZONTAL; layoutParams = lp.apply { column = 5 }; text = rh.gs( - app.aaps.core.ui.R.string.format_carbs, carbs - .toInt() - ) - }) - } - } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/stats/DexcomTirImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/stats/DexcomTirImplTest.kt index fda21e723eb0..360f8e5770a7 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/stats/DexcomTirImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/stats/DexcomTirImplTest.kt @@ -1,22 +1,10 @@ package app.aaps.implementation.stats -import app.aaps.core.interfaces.profile.ProfileUtil import com.google.common.truth.Truth.assertThat import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [35]) internal class DexcomTirImplTest { - // Unstubbed: its results only feed getString/StringBuilder in the view builders, which tolerate null. - private val profileUtil: ProfileUtil = mock() - private val context get() = RuntimeEnvironment.getApplication() - /** Builds a TIR with one reading in each band + one error, using values that fall in the same band * whether the timestamp is treated as day or night (so the assertions are timezone-independent). */ private fun populated(): DexcomTirImpl = DexcomTirImpl().apply { @@ -67,20 +55,4 @@ internal class DexcomTirImplTest { assertThat(tir.highTirMgdl()).isLessThan(tir.veryHighTirMgdl()) assertThat(tir.highNightTirMgdl()).isLessThan(tir.highTirMgdl()) } - - @Test - fun `view builders produce the expected structure`() { - val tir = populated() - assertThat(tir.toTableRowHeader(context).childCount).isEqualTo(5) - assertThat(tir.toTableRow(context).childCount).isEqualTo(5) - // HbA1c/SD/range-header text views build without throwing. - assertThat(tir.toHbA1cView(context)).isNotNull() - assertThat(tir.toSDView(context, profileUtil)).isNotNull() - assertThat(tir.toRangeHeaderView(context, profileUtil)).isNotNull() - } - - @Test - fun `HbA1c view is empty when there are no readings`() { - assertThat(DexcomTirImpl().toHbA1cView(context).text.toString()).isEmpty() - } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/stats/TirImplViewTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/stats/TirImplViewTest.kt deleted file mode 100644 index 033b584fab43..000000000000 --- a/implementation/src/test/kotlin/app/aaps/implementation/stats/TirImplViewTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.aaps.implementation.stats - -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DateUtil -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.anyLong -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config - -/** - * Covers [TirImpl]'s Android view builders (`toTableRow` / `toTableRowHeader`), which need a real - * [android.content.Context] — the counter logic is covered separately by [TirImplTest] (plain JVM). - */ -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [35]) -internal class TirImplViewTest { - - private val rh: ResourceHelper = mock() - private val dateUtil: DateUtil = mock() - private val context get() = RuntimeEnvironment.getApplication() - - @Before - fun setUp() { - whenever(rh.gs(anyInt())).thenReturn("x") - whenever(rh.gs(anyInt(), any())).thenReturn("x") - whenever(dateUtil.dateStringShort(anyLong())).thenReturn("d") - } - - @Test - fun `table rows have a date column plus below, in-range and above columns`() { - val tir = TirImpl(1_600_000_000_000L, 70.0, 180.0).apply { below(); inRange(); above() } - - assertThat(TirImpl.toTableRowHeader(context, rh).childCount).isEqualTo(4) - assertThat(tir.toTableRow(context, rh, dateUtil).childCount).isEqualTo(4) - assertThat(tir.toTableRow(context, rh, days = 7).childCount).isEqualTo(4) - } -} From b3bf3633524238c422f677f2ee2f14d49cb290a2 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 10 Aug 2026 22:57:53 +0200 Subject: [PATCH 047/146] :core:interfaces eliminate redundant Context parameters --- .../kotlin/app/aaps/ComposeMainActivity.kt | 2 +- app/src/main/kotlin/app/aaps/MainApp.kt | 4 +-- .../maintenance/ImportExportPrefs.kt | 4 +-- .../notifications/NotificationHolder.kt | 3 +-- .../protection/ExportPasswordDataStore.kt | 10 +++----- .../aaps/core/interfaces/pump/BlePreCheck.kt | 3 +-- .../core/ui/compose/pump/BlePreCheckHost.kt | 2 +- .../NotificationHolderImpl.kt | 4 +-- .../maintenance/ImportExportPrefsImpl.kt | 18 ++++++------- .../notifications/NotificationManagerImpl.kt | 2 +- .../protection/ExportPasswordDataStoreImpl.kt | 25 ++++++++++--------- .../protection/PasswordCheckImpl.kt | 2 +- .../implementation/pump/BlePreCheckImpl.kt | 2 +- .../ExportPasswordDataStoreImplTest.kt | 14 +++++------ .../actions/ActionSettingsExport.kt | 6 ++--- .../PersistentNotificationPlugin.kt | 2 +- .../ui/compose/treatments/UserEntryScreen.kt | 2 +- 17 files changed, 51 insertions(+), 54 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt index f63a200c7f73..2f5583cbdd66 100644 --- a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt +++ b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt @@ -334,7 +334,7 @@ class ComposeMainActivity : AppCompatActivity() { LocalProfileUtil provides profileUtil, LocalCheckPassword provides cryptoUtil::checkPassword, LocalHashPassword provides cryptoUtil::hashPassword, - LocalClearExportPasswordStore provides { exportPasswordDataStore.clearPasswordDataStore(this@ComposeMainActivity) }, + LocalClearExportPasswordStore provides { exportPasswordDataStore.clearPasswordDataStore() }, LocalVisibilityContext provides visibilityContext ) { AapsTheme { diff --git a/app/src/main/kotlin/app/aaps/MainApp.kt b/app/src/main/kotlin/app/aaps/MainApp.kt index 723f0f8b30c5..356ed9048cd7 100644 --- a/app/src/main/kotlin/app/aaps/MainApp.kt +++ b/app/src/main/kotlin/app/aaps/MainApp.kt @@ -447,7 +447,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { if (serialNumber != null) { preferences.put(StringKey.ProtectionMasterPassword, cryptoUtil.hashPassword(serialNumber)) fh.delete() - exportPasswordDataStore.clearPasswordDataStore(this@MainApp) + exportPasswordDataStore.clearPasswordDataStore() config.showInitSnackbar(getString(app.aaps.core.ui.R.string.password_set)) } else { aapsLogger.warn(LTag.CORE, "Password reset timed out waiting for pump serial number") @@ -458,7 +458,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { private fun exportPasswordResetCheck() { val fh = fileListProvider.ensureExtraDirExists()?.findFile("ExportPasswordReset") if (fh?.exists() == true) { - exportPasswordDataStore.clearPasswordDataStore(this@MainApp) + exportPasswordDataStore.clearPasswordDataStore() fh.delete() config.showInitSnackbar(getString(app.aaps.core.ui.R.string.datastore_password_cleared)) } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt index 004669f21eba..84877d44fa27 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt @@ -51,8 +51,8 @@ interface ImportExportPrefs { fun exportCustomWatchface(customWatchface: CwfData, withDate: Boolean = true) fun exportSharedPreferences(activity: FragmentActivity) - fun exportSharedPreferencesNonInteractive(context: Context, password: String): Boolean - fun exportUserEntriesCsv(context: Context) + fun exportSharedPreferencesNonInteractive(password: String): Boolean + fun exportUserEntriesCsv() suspend fun executeCsvExport(): ExportResult fun exportApsResult(algorithm: String?, input: JSONObject, output: JSONObject?) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt index 862b1ca9199c..c3527b82cf37 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt @@ -2,7 +2,6 @@ package app.aaps.core.interfaces.notifications import android.app.Notification import android.app.PendingIntent -import android.content.Context interface NotificationHolder { @@ -10,6 +9,6 @@ interface NotificationHolder { val notificationID: Int var notification: Notification - fun openAppIntent(context: Context): PendingIntent? + fun openAppIntent(): PendingIntent? fun createNotificationChannel() } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt index 375a8a509998..527519f77137 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt @@ -1,7 +1,5 @@ package app.aaps.core.interfaces.protection -import android.content.Context - interface ExportPasswordDataStore { /*** @@ -13,17 +11,17 @@ interface ExportPasswordDataStore { /*** * Clear password currently stored. */ - fun clearPasswordDataStore(context: Context): String + fun clearPasswordDataStore(): String /*** * Put password to local phone's datastore. */ - fun putPasswordToDataStore(context: Context, password: String): String + fun putPasswordToDataStore(password: String): String /*** * Get password from local phone's data store. * Return pair (true,) or (false,"") */ - fun getPasswordFromDataStore(context: Context): Triple + fun getPasswordFromDataStore(): Triple - } \ No newline at end of file + } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt index 9e2895de5c72..0be9e7f0b893 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.pump -import android.content.Context import androidx.appcompat.app.AppCompatActivity interface BlePreCheck { @@ -12,6 +11,6 @@ interface BlePreCheck { * Attempts to enable Bluetooth if permissions are granted. * Returns [BlePreCheckResult] indicating the current state. */ - fun checkBleReady(context: Context): BlePreCheckResult + fun checkBleReady(): BlePreCheckResult } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt index a70aacbfa0ec..4cc57ddca2c9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt @@ -41,7 +41,7 @@ fun BlePreCheckHost( LaunchedEffect(Unit) { checkResult = withContext(Dispatchers.IO) { - blePreCheck.checkBleReady(context) + blePreCheck.checkBleReady() } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt index ed304d824b7c..ec309ec62613 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt @@ -32,7 +32,7 @@ class NotificationHolderImpl @Inject constructor( } get() = _notification ?: placeholderNotification() - override fun openAppIntent(context: Context): PendingIntent? = TaskStackBuilder.create(context).run { + override fun openAppIntent(): PendingIntent? = TaskStackBuilder.create(context).run { addParentStack(uiInteraction.mainActivity) addNextIntent(Intent(context, uiInteraction.mainActivity)) getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) @@ -49,7 +49,7 @@ class NotificationHolderImpl @Inject constructor( .setSmallIcon(iconsProvider.getNotificationIcon()) .setLargeIcon(rh.decodeResource(iconsProvider.getIcon())) .setContentTitle(rh.gs(app.aaps.core.ui.R.string.loading)) - .setContentIntent(openAppIntent(context)) + .setContentIntent(openAppIntent()) .build() .also { (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(notificationID, it) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt index 75dbfb58d03f..5551f1398246 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt @@ -214,9 +214,9 @@ class ImportExportPrefsImpl @Inject constructor( pendingExportFile = newFile } - val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore(context) + val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore() val cachedPassword = if (password.isNotEmpty() && !(isExpired || isAboutToExpire)) password else { - exportPasswordDataStore.clearPasswordDataStore(context) + exportPasswordDataStore.clearPasswordDataStore() null } @@ -342,7 +342,7 @@ class ImportExportPrefsImpl @Inject constructor( } override fun cacheExportPassword(password: String): String = - exportPasswordDataStore.putPasswordToDataStore(context, password) + exportPasswordDataStore.putPasswordToDataStore(password) // Legacy export — uses dialogs via uiInteraction (kept for old UI) @@ -404,7 +404,7 @@ class ImportExportPrefsImpl @Inject constructor( onPositive = { passwordCheck.setPassword(activity, StringKey.ProtectionMasterPassword.title, StringKey.ProtectionMasterPassword) } ) ) - exportPasswordDataStore.clearPasswordDataStore(context) + exportPasswordDataStore.clearPasswordDataStore() return false } return true @@ -419,7 +419,7 @@ class ImportExportPrefsImpl @Inject constructor( } // Get password from datastore - val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore(context) + val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore() if (password.isNotEmpty() && !(isExpired || isAboutToExpire)) { // We have an (encrypted) password in the phones DataStore that is not expired or about to expire (third) then(password) @@ -427,7 +427,7 @@ class ImportExportPrefsImpl @Inject constructor( } // Make sure stored password is properly reset - exportPasswordDataStore.clearPasswordDataStore((context)) + exportPasswordDataStore.clearPasswordDataStore() // Ask for entering password and store when successfully entered rxBus.send( @@ -439,7 +439,7 @@ class ImportExportPrefsImpl @Inject constructor( onOk = { askForMasterPassIfNeeded(activity, app.aaps.core.ui.R.string.preferences_export_canceled) { password -> - then(exportPasswordDataStore.putPasswordToDataStore(context, password)) + then(exportPasswordDataStore.putPasswordToDataStore(password)) } } ) @@ -705,7 +705,7 @@ class ImportExportPrefsImpl @Inject constructor( } } - override fun exportSharedPreferencesNonInteractive(context: Context, password: String): Boolean { + override fun exportSharedPreferencesNonInteractive(password: String): Boolean { // Check export destination preferences (same logic as manual export) val localEnabled = preferences.get(BooleanNonKey.ExportSettingsLocalEnabled) val cloudEnabled = preferences.get(BooleanNonKey.ExportSettingsCloudEnabled) @@ -926,7 +926,7 @@ class ImportExportPrefsImpl @Inject constructor( preferences.put(BooleanNonKey.GeneralSetupWizardProcessed, true) } - override fun exportUserEntriesCsv(context: Context) { + override fun exportUserEntriesCsv() { aapsLogger.info(LTag.CORE, "${CloudConstants.LOG_PREFIX} CSV_EXPORT exportUserEntriesCsv called, enqueuing WorkManager") WorkManager.getInstance(context).enqueueUniqueWork( "export", diff --git a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt index 9f8a118787c3..a845bec0e4c9 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt @@ -350,7 +350,7 @@ class NotificationManagerImpl @Inject constructor( .setStyle(NotificationCompat.BigTextStyle().bigText(n.text)) .setPriority(NotificationCompat.PRIORITY_MAX) .setDeleteIntent(deleteIntent(n.id.ordinal)) - .setContentIntent(notificationHolder.openAppIntent(context)) + .setContentIntent(notificationHolder.openAppIntent()) if (n.level == NotificationLevel.URGENT) { notificationBuilder.setVibrate(longArrayOf(1000, 1000, 1000, 1000)) .setContentTitle(rh.gs(app.aaps.core.ui.R.string.urgent_alarm)) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImpl.kt index 22fc2dbb6d2f..511984f1fbcb 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImpl.kt @@ -39,6 +39,7 @@ import javax.inject.Singleton @Singleton class ExportPasswordDataStoreImpl @Inject constructor( + private val context: Context, private var aapsLogger: AAPSLogger, private var preferences: Preferences, private var config: Config @@ -127,32 +128,32 @@ class ExportPasswordDataStoreImpl @Inject constructor( /*** * Clear password currently stored in DataStore to "empty" */ - override fun clearPasswordDataStore(context: Context): String { + override fun clearPasswordDataStore(): String { if (!exportPasswordStoreEnabled()) return "" // Do nothing, return empty // Store & update to empty password and return aapsLogger.debug(LTag.CORE, "$MODULE: clearPasswordDataStore") - return this.clearPassword(context) + return this.clearPassword() } /*** * Store password in local phone's DataStore * Return: password */ - override fun putPasswordToDataStore(context: Context, password: String): String { + override fun putPasswordToDataStore(password: String): String { if (!exportPasswordStoreEnabled()) return password // Just return the password aapsLogger.debug(LTag.CORE, "$MODULE: putPasswordToDataStore") - return this.storePassword(context, password) + return this.storePassword(password) } /*** * Get password from local phone's DataStore * Return Triple (ok, password string, isExpired, isAboutToExpire) */ - override fun getPasswordFromDataStore(context: Context): Triple { + override fun getPasswordFromDataStore(): Triple { if (!exportPasswordStoreEnabled()) return Triple("", true, true) - val passwordData = this.retrievePassword(context) + val passwordData = this.retrievePassword() with(passwordData) { if (password.isNotEmpty()) { // And not expired // The stored password must stay in sync with the master password. Decrypt the stored secret and @@ -162,7 +163,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( val masterHash = preferences.getIfExists(StringKey.ProtectionMasterPassword) if (masterHash.isNullOrEmpty() || !cryptoUtil.checkPassword(secureEncrypt.decrypt(password), masterHash)) { aapsLogger.info(LTag.CORE, "$MODULE: stored password no longer matches the master password, clearing") - clearPasswordDataStore(context) + clearPasswordDataStore() return Triple("", true, true) } aapsLogger.debug(LTag.CORE, "$MODULE: getPasswordFromDataStore") @@ -188,7 +189,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( /*** * Clear password and timestamp */ - private fun clearPassword(context: Context): String { + private fun clearPassword(): String { // Write setting to android datastore and return password fun updatePrefString(name: String) = runBlocking { @@ -206,7 +207,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( /*** * Store password and set timestamp to current */ - private fun storePassword(context: Context, password: String): String { + private fun storePassword(password: String): String { // Write encrypted password key and timestamp to the local phone's android datastore and return password fun updatePrefString(name: String, str: String) = runBlocking { @@ -226,7 +227,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( * Retrieve password from local phone's data store. * Reset password when validity expired ***/ - private fun retrievePassword(context: Context): ClassPasswordData { + private fun retrievePassword(): ClassPasswordData { // Read encrypted password key and timestamp from the local phone's android datastore and return password var passwordStr = "" @@ -247,7 +248,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( val aliasInBlob = passwordStr.split(":").getOrNull(1) if (aliasInBlob != null && aliasInBlob != KEYSTORE_ALIAS) { aapsLogger.info(LTag.CORE, "$MODULE: legacy alias '$aliasInBlob' in stored password, clearing for re-entry with hardened key") - clearPassword(context) + clearPassword() passwordStr = "" timestampStr = "" secureEncrypt.deleteKey(aliasInBlob) @@ -271,7 +272,7 @@ class ExportPasswordDataStoreImpl @Inject constructor( isAboutToExpire = expires // When expired, need to renew: clear/update password in data store - if (isExpired) password = clearPasswordDataStore(context) + if (isExpired) password = clearPasswordDataStore() } // Store/update password and return return classPasswordData diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt index ad2cb57b2441..1eb76a141ba2 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/PasswordCheckImpl.kt @@ -192,7 +192,7 @@ class PasswordCheckImpl @Inject constructor( rxBus.send(EventShowSnackbar(context.getString(msg), EventShowSnackbar.Type.Error)) } else if (enteredPassword.isNotEmpty()) { preferences.put(preference, cryptoUtil.hashPassword(enteredPassword)) - exportPasswordDataStore.clearPasswordDataStore(context) + exportPasswordDataStore.clearPasswordDataStore() val msg = if (pinInput) app.aaps.core.ui.R.string.pin_set else app.aaps.core.ui.R.string.password_set rxBus.send(EventShowSnackbar(context.getString(msg), EventShowSnackbar.Type.Success)) dialog.dismiss() diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/BlePreCheckImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/BlePreCheckImpl.kt index c371cf686128..94bc9239a78b 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/BlePreCheckImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/BlePreCheckImpl.kt @@ -59,7 +59,7 @@ class BlePreCheckImpl @Inject constructor( return true } - override fun checkBleReady(context: Context): BlePreCheckResult { + override fun checkBleReady(): BlePreCheckResult { if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { return BlePreCheckResult.BLE_NOT_SUPPORTED } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImplTest.kt index 0ccbbc1f9a6a..05cd0c5bc0e5 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/protection/ExportPasswordDataStoreImplTest.kt @@ -12,7 +12,7 @@ class ExportPasswordDataStoreImplTest : TestBaseWithProfile() { private val somePassword = "somePassword" val sut: ExportPasswordDataStoreImpl by lazy { - ExportPasswordDataStoreImpl(aapsLogger, preferences, config) + ExportPasswordDataStoreImpl(context, aapsLogger, preferences, config) } @Test @@ -24,28 +24,28 @@ class ExportPasswordDataStoreImplTest : TestBaseWithProfile() { // When enabled whenever(preferences.get(BooleanKey.MaintenanceEnableExportSettingsAutomation)).thenReturn(true) assertTrue(sut.exportPasswordStoreEnabled()) - assertTrue(sut.clearPasswordDataStore(context).isEmpty()) + assertTrue(sut.clearPasswordDataStore().isEmpty()) // These will fail to run (can not instantiate secure encrypt?) - // assertTrue(sut.putPasswordToDataStore(context, somePassword) == somePassword) - // assertTrue(sut.getPasswordFromDataStore(context) == Triple ("", true, true)) + // assertTrue(sut.putPasswordToDataStore(somePassword) == somePassword) + // assertTrue(sut.getPasswordFromDataStore() == Triple ("", true, true)) } @Test fun clearPasswordDataStore() { whenever(preferences.get(BooleanKey.MaintenanceEnableExportSettingsAutomation)).thenReturn(false) - assertTrue(sut.clearPasswordDataStore(context).isEmpty()) + assertTrue(sut.clearPasswordDataStore().isEmpty()) } @Test fun putPasswordToDataStore() { whenever(preferences.get(BooleanKey.MaintenanceEnableExportSettingsAutomation)).thenReturn(false) - assertTrue(sut.putPasswordToDataStore(context, somePassword) == somePassword) + assertTrue(sut.putPasswordToDataStore(somePassword) == somePassword) } @Test fun getPasswordFromDataStore() { whenever(preferences.get(BooleanKey.MaintenanceEnableExportSettingsAutomation)).thenReturn(false) - assertTrue(sut.getPasswordFromDataStore(context) == Triple("", true, true)) + assertTrue(sut.getPasswordFromDataStore() == Triple("", true, true)) } } \ No newline at end of file diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionSettingsExport.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionSettingsExport.kt index 9719b19b87e8..d5d8722bd23b 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionSettingsExport.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionSettingsExport.kt @@ -61,7 +61,7 @@ class ActionSettingsExport(injector: HasAndroidInjector) : Action(injector) { if (exportPasswordDataStore.exportPasswordStoreEnabled()) { // Get the (encrypted) password and status from the DataStore - val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore(context) + val (password, isExpired, isAboutToExpire) = exportPasswordDataStore.getPasswordFromDataStore() aapsLogger.debug(LTag.AUTOMATION, "Exporting settings: passwordIsNotEmpty=${password.isNotEmpty()}, isExpired=$isExpired, isAboutToExpire=$isAboutToExpire") // And do according to password state @@ -80,7 +80,7 @@ class ActionSettingsExport(injector: HasAndroidInjector) : Action(injector) { exportResultLevel = NotificationLevel.INFO // INFO -> e.g. color GREEN } // Execute settings export, then notify user - if (!importExportPrefs.exportSharedPreferencesNonInteractive(context, password)) { + if (!importExportPrefs.exportSharedPreferencesNonInteractive(password)) { // :-( Export failed (see logfile!?) aapsLogger.error(LTag.AUTOMATION, "ERROR: exportSharedPreferencesNonInteractive() failed to export settings") exportResultComment = app.aaps.core.ui.R.string.export_failed @@ -95,7 +95,7 @@ class ActionSettingsExport(injector: HasAndroidInjector) : Action(injector) { exportResultLevel = NotificationLevel.IMPORTANT // URGENT -> e.g. color RED // Clear password in datastore, then notify user aapsLogger.info(LTag.AUTOMATION, "No password or was expired and needs re-entering by user") - exportPasswordDataStore.clearPasswordDataStore(context) + exportPasswordDataStore.clearPasswordDataStore() announceAlert = true } } else { diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index 13a81b099f4b..e36ee80e8d16 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -252,7 +252,7 @@ class PersistentNotificationPlugin @Inject constructor( ) } /// End Android Auto - builder.setContentIntent(notificationHolder.openAppIntent(context)) + builder.setContentIntent(notificationHolder.openAppIntent()) val mNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notification = builder.build() mNotificationManager.notify(notificationHolder.notificationID, notification) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/UserEntryScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/UserEntryScreen.kt index 8c6583c4d5fc..3bfe1ab4ed3e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/UserEntryScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/UserEntryScreen.kt @@ -118,7 +118,7 @@ fun UserEntryScreen( message = viewModel.rh.gs(app.aaps.core.ui.R.string.ue_export_to_csv) + "?", onConfirm = { uel.log(Action.EXPORT_CSV, Sources.Treatments) - importExportPrefs.exportUserEntriesCsv(context) + importExportPrefs.exportUserEntriesCsv() showExportDialog = false }, onDismiss = { showExportDialog = false } From ce8d14a3cd132c7ccc260de7af0fa100157af72d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 07:39:20 +0200 Subject: [PATCH 048/146] :core:interfaces EventStatus returns TextRef --- .../kotlin/app/aaps/ComposeMainActivity.kt | 2 +- .../rx/events/EventPumpStatusChanged.kt | 21 ++++++++++--------- .../interfaces/rx/events/EventSWRLStatus.kt | 4 ++-- .../interfaces/rx/events/EventSWSyncStatus.kt | 4 ++-- .../core/interfaces/rx/events/EventStatus.kt | 7 +++---- .../compose/pump/PumpCommunicationStatus.kt | 6 +++--- .../setupwizard/SWEventListener.kt | 11 +++++----- .../compose/ComboV2OverviewViewModel.kt | 2 +- .../dana/compose/DanaOverviewViewModel.kt | 2 +- .../compose/DiaconnHistoryViewModel.kt | 2 +- .../compose/DiaconnOverviewViewModel.kt | 2 +- .../compose/EopatchOverviewViewModel.kt | 2 +- .../equil/compose/EquilOverviewViewModel.kt | 2 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 2 +- .../compose/MedtronicOverviewViewModel.kt | 2 +- .../compose/MedtrumOverviewViewModel.kt | 2 +- .../dash/ui/compose/DashOverviewViewModel.kt | 2 +- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 2 +- .../eros/ui/compose/ErosOverviewViewModel.kt | 2 +- .../EventRileyLinkDeviceStatusChange.kt | 12 +++++------ .../aaps/pump/virtual/VirtualPumpViewModel.kt | 2 +- 21 files changed, 47 insertions(+), 46 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt index 2f5583cbdd66..7e9c8086c7d0 100644 --- a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt +++ b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt @@ -248,7 +248,7 @@ class ComposeMainActivity : AppCompatActivity() { private val siteRotationManagementViewModel: SiteRotationManagementViewModel by viewModels() private val pumpCommunicationStatus by lazy { - PumpCommunicationStatus(rxBus, commandQueue, this, lifecycleScope) + PumpCommunicationStatus(rxBus, commandQueue, rh, lifecycleScope) } private var navController: NavHostController? = null private val _autoShowNotifications = mutableStateOf(false) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt index 30c9f68111be..a40ef67c3e47 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt @@ -1,7 +1,8 @@ package app.aaps.core.interfaces.rx.events -import android.content.Context -import app.aaps.core.interfaces.R +import app.aaps.core.interfaces.InterfacesStrings +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs /** * Fired when the pump connection status changes. @@ -90,15 +91,15 @@ class EventPumpStatusChanged : EventStatus { /** * Gets a human-readable status message for the startup wizard. */ - override fun getStatus(context: Context): String { + override fun getStatus(): TextRef { return when (status) { - Status.CONNECTING -> context.getString(R.string.connecting_for, secondsElapsed) - Status.HANDSHAKING -> context.getString(R.string.handshaking) - Status.CONNECTED -> context.getString(R.string.connected) - Status.PERFORMING -> performingAction - Status.WAITING_FOR_DISCONNECTION -> context.getString(R.string.waiting_for_disconnection) - Status.DISCONNECTING -> context.getString(R.string.disconnecting) - Status.DISCONNECTED -> "" + Status.CONNECTING -> InterfacesStrings.connecting_for.withArgs(secondsElapsed) + Status.HANDSHAKING -> InterfacesStrings.handshaking + Status.CONNECTED -> InterfacesStrings.connected + Status.PERFORMING -> TextRef.Literal(performingAction) + Status.WAITING_FOR_DISCONNECTION -> InterfacesStrings.waiting_for_disconnection + Status.DISCONNECTING -> InterfacesStrings.disconnecting + Status.DISCONNECTED -> TextRef.Literal("") } } } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt index 19236d831011..36f2ae63fdfc 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.rx.events -import android.content.Context +import app.aaps.core.keys.interfaces.TextRef /** * Fired to update the setup wizard with the RileyLink status. @@ -9,5 +9,5 @@ import android.content.Context */ class EventSWRLStatus(val status: String) : EventStatus() { - override fun getStatus(context: Context): String = status + override fun getStatus(): TextRef = TextRef.Literal(status) } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt index 55b1586de8c5..20a3b8d85162 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.rx.events -import android.content.Context +import app.aaps.core.keys.interfaces.TextRef /** * Fired to update the setup wizard with the sync status. @@ -9,5 +9,5 @@ import android.content.Context */ class EventSWSyncStatus(val status: String) : EventStatus() { - override fun getStatus(context: Context): String = status + override fun getStatus(): TextRef = TextRef.Literal(status) } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt index c45ea1c0f1a2..33058b4d1e47 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.rx.events -import android.content.Context +import app.aaps.core.keys.interfaces.TextRef /** * Base class for events that carry a status message used in UI updates @@ -10,8 +10,7 @@ abstract class EventStatus : Event() { /** * Gets the status message. * - * @param context The context. - * @return The status message string. + * @return The status message, as a platform neutral reference. */ - abstract fun getStatus(context: Context): String + abstract fun getStatus(): TextRef } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt index 051357459e44..bd191cc4dbcb 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt @@ -1,6 +1,6 @@ package app.aaps.core.ui.compose.pump -import android.content.Context +import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.onEach class PumpCommunicationStatus( rxBus: RxBus, private val commandQueue: CommandQueue, - private val context: Context, + private val rh: ResourceHelper, scope: CoroutineScope ) { @@ -39,7 +39,7 @@ class PumpCommunicationStatus( init { rxBus.toFlow(EventPumpStatusChanged::class.java) .onEach { event -> - val text = event.getStatus(context) + val text = rh.gs(event.getStatus()) _statusBanner.value = if (text.isEmpty()) null else StatusBanner(text = text, level = StatusLevel.UNSPECIFIED) refreshTrigger.value = System.currentTimeMillis() } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt index b921cc55b115..3e98ca1035f7 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.protection.PasswordCheck @@ -58,18 +57,20 @@ class SWEventListener @Inject constructor( @Composable override fun Compose() { if (visibilityValidator?.invoke() == false) return - val context = LocalContext.current - val statusState = remember { mutableStateOf(status) } + // The event carries a TextRef now, so it is held unresolved and turned into text here, in + // the Composable. That keeps the resolving out of the Rx callback, which had to reach for a + // Context purely to read a string. + val statusState = remember { mutableStateOf(TextRef.Literal(status)) } DisposableEffect(clazz) { val disposable = rxBus .toObservable(clazz) .observeOn(AndroidSchedulers.mainThread()) .subscribe { event -> - statusState.value = event.getStatus(context) + statusState.value = event.getStatus() } onDispose { disposable.dispose() } } val labelText = textLabel?.let { stringResource(it) } ?: "" - Text(text = "$labelText ${statusState.value}".trim()) + Text(text = "$labelText ${stringResource(statusState.value)}".trim()) } } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModel.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModel.kt index a56096420845..54f1cf101284 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModel.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModel.kt @@ -72,7 +72,7 @@ class ComboV2OverviewViewModel @Inject constructor( @ApplicationContext context: Context ) : ViewModel() { - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, viewModelScope) private data class PumpSnapshot( val isPaired: Boolean, diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt index a598e1c28fa7..7cc6cefd1aba 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt @@ -90,7 +90,7 @@ open class DanaOverviewViewModel @Inject constructor( private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, viewModelScope) // RxBus events converted to a flow trigger for recomposition protected val rxTrigger = MutableStateFlow(0L) diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt index 55232c429e92..625e00d4f721 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt @@ -67,7 +67,7 @@ class DiaconnHistoryViewModel @Inject constructor( .toObservable(EventPumpStatusChanged::class.java) .observeOn(aapsSchedulers.main) .subscribe({ event -> - _uiState.update { it.copy(statusMessage = event.getStatus(context)) } + _uiState.update { it.copy(statusMessage = rh.gs(event.getStatus())) } }, { aapsLogger.error(LTag.PUMP, "Error", it) }) types.firstOrNull()?.let { loadRecords(it.type) } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt index 68da2272d431..c4d03aa9cde4 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt @@ -90,7 +90,7 @@ class DiaconnOverviewViewModel @Inject constructor( private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, viewModelScope) private val rxTrigger = MutableStateFlow(0L) diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModel.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModel.kt index 04bcb5df7f9a..35c8286c178c 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModel.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModel.kt @@ -94,7 +94,7 @@ class EopatchOverviewViewModel @Inject constructor( private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val stateBuilder = PumpOverviewStateBuilder(rh) - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, scope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, scope) private val disposables = CompositeDisposable() private val _events = MutableSharedFlow(extraBufferCapacity = 5) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt index ae31f2f13700..4b4851b2d1f5 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt @@ -78,7 +78,7 @@ class EquilOverviewViewModel @Inject constructor( private val _isModeChanging = MutableStateFlow(false) val isModeChanging: StateFlow = _isModeChanging - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, viewModelScope) // Trigger re-composition on RxBus events and periodic ticks private val _refreshTrigger = MutableStateFlow(0L) diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index 4c5eff8e4115..42c2b8c85688 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -214,7 +214,7 @@ class MedtronicPumpPlugin @Inject constructor( rxBus .toObservable(EventRileyLinkDeviceStatusChange::class.java) .observeOn(aapsSchedulers.io) - .subscribe({ event: EventRileyLinkDeviceStatusChange -> rxBus.send(EventSWRLStatus(event.getStatus(context))) }, fabricPrivacy::logException) + .subscribe({ event: EventRileyLinkDeviceStatusChange -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) }, fabricPrivacy::logException) ) val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt index 8d2d06b3f0d0..194bf52a1969 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt @@ -92,7 +92,7 @@ class MedtronicOverviewViewModel @Inject constructor( private const val PLACEHOLDER = "-" } - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, viewModelScope) private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt index 82dc27f3f051..b98d6df2d64b 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt @@ -83,7 +83,7 @@ class MedtrumOverviewViewModel @Inject constructor( ) : ViewModel() { private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, scope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, scope) private val stateBuilder = PumpOverviewStateBuilder(rh) private val _events = MutableSharedFlow(extraBufferCapacity = 5) diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt index 063952e68adb..d2f3d2238eb4 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt @@ -105,7 +105,7 @@ class DashOverviewViewModel @Inject constructor( } private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, scope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, scope) private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index 5d5f0d75b7c6..e1799a8dc5e1 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -280,7 +280,7 @@ class OmnipodErosPumpPlugin @Inject constructor( disposable += rxBus .toObservable(EventRileyLinkDeviceStatusChange::class.java) .observeOn(aapsSchedulers.io) - .subscribe({ event -> rxBus.send(EventSWRLStatus(event.getStatus(context))) }, fabricPrivacy::logException) + .subscribe({ event -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) }, fabricPrivacy::logException) val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope merge( diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt index e055dc7d43f5..4e7f8c2c4ce1 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt @@ -122,7 +122,7 @@ class ErosOverviewViewModel @Inject constructor( } private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, scope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, scope) private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/events/EventRileyLinkDeviceStatusChange.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/events/EventRileyLinkDeviceStatusChange.kt index 79b645c3abae..70f8efdf8258 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/events/EventRileyLinkDeviceStatusChange.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/events/EventRileyLinkDeviceStatusChange.kt @@ -1,6 +1,6 @@ package app.aaps.pump.common.events -import android.content.Context +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.pump.defs.PumpDeviceState import app.aaps.core.interfaces.rx.events.EventStatus import app.aaps.pump.common.hw.rileylink.defs.RileyLinkError @@ -33,16 +33,16 @@ open class EventRileyLinkDeviceStatusChange : EventStatus { this.errorDescription = errorDescription } - override fun getStatus(context: Context): String { - val rileyLinkServiceState = this.rileyLinkServiceState ?: return "" + override fun getStatus(): TextRef { + val rileyLinkServiceState = this.rileyLinkServiceState ?: return TextRef.Literal("") val resourceId = rileyLinkServiceState.resourceId val rileyLinkError = this.rileyLinkError if (rileyLinkServiceState.isError() && rileyLinkError != null) { - val rileyLinkTargetDevice = this.rileyLinkTargetDevice ?: return "" - return context.getString(rileyLinkError.getResourceId(rileyLinkTargetDevice)) + val rileyLinkTargetDevice = this.rileyLinkTargetDevice ?: return TextRef.Literal("") + return TextRef.AndroidRes(rileyLinkError.getResourceId(rileyLinkTargetDevice)) } - return context.getString(resourceId) + return TextRef.AndroidRes(resourceId) } } diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt index 6d62f6d8366e..80db51f9d0c6 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt @@ -55,7 +55,7 @@ class VirtualPumpViewModel( ) { private val stateBuilder = PumpOverviewStateBuilder(rh) - private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, scope) + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, rh, scope) // VirtualPump has no hardware — it relies on DB to know active TB/EB private val dbChanged = merge( From b0922302841a9710ff7a0dff3d99d8556abf56c9 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 07:43:58 +0200 Subject: [PATCH 049/146] :core:interfaces Clock instead of System.currentTimeMillis --- .../src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt | 3 ++- .../kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt | 3 ++- .../aaps/core/interfaces/notifications/AapsNotification.kt | 3 ++- .../core/interfaces/notifications/NotificationManager.kt | 5 +++-- .../kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt | 3 ++- .../app/aaps/core/interfaces/pump/DetailedBolusInfo.kt | 5 +++-- .../main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt | 5 +++-- .../kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt | 5 +++-- .../kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt | 3 ++- 9 files changed, 22 insertions(+), 13 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt index 40fed3418443..732b154c3973 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt @@ -7,6 +7,7 @@ import app.aaps.core.data.ue.ValueWithUnit import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.PumpEnactResult +import kotlin.time.Clock interface Loop { @@ -26,7 +27,7 @@ interface Loop { var tbrSetByPump: PumpEnactResult? = null var smbSetByPump: PumpEnactResult? = null var source: String? = null - var lastAPSRun = System.currentTimeMillis() + var lastAPSRun = Clock.System.now().toEpochMilliseconds() var lastTBREnact: Long = 0 var lastSMBEnact: Long = 0 var lastTBRRequest: Long = 0 diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt index 0a143b5c9297..7ba152c5bbb8 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt @@ -29,6 +29,7 @@ import app.aaps.core.data.ue.ValueWithUnit import app.aaps.core.interfaces.aps.APSResult import kotlinx.coroutines.flow.Flow import kotlin.reflect.KClass +import kotlin.time.Clock /** * Read-only diagnostics gathered before a startup VACUUM. @@ -1290,7 +1291,7 @@ interface PersistenceLayer { */ suspend fun insertPumpTherapyEventIfNewByTimestamp( therapyEvent: TE, - timestamp: Long = System.currentTimeMillis(), + timestamp: Long = Clock.System.now().toEpochMilliseconds(), action: Action, source: Sources, note: String?, diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt index fea873d4334f..07986a591b8d 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt @@ -1,13 +1,14 @@ package app.aaps.core.interfaces.notifications import androidx.annotation.RawRes +import kotlin.time.Clock data class AapsNotification( val id: NotificationId, val instanceKey: Int, val text: String, val level: NotificationLevel, - val date: Long = System.currentTimeMillis(), + val date: Long = Clock.System.now().toEpochMilliseconds(), val validTo: Long = 0L, @RawRes val soundRes: Int? = null, val actions: List = emptyList(), diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt index 9ca9f100e1cc..33411f9bbc3f 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt @@ -3,6 +3,7 @@ package app.aaps.core.interfaces.notifications import androidx.annotation.RawRes import androidx.annotation.StringRes import kotlinx.coroutines.flow.StateFlow +import kotlin.time.Clock interface NotificationManager { @@ -25,7 +26,7 @@ interface NotificationManager { id: NotificationId, text: String, level: NotificationLevel = id.defaultLevel, - date: Long = System.currentTimeMillis(), + date: Long = Clock.System.now().toEpochMilliseconds(), validTo: Long = 0L, @RawRes soundRes: Int? = null, actions: List = emptyList(), @@ -38,7 +39,7 @@ interface NotificationManager { vararg formatArgs: Any?, level: NotificationLevel = id.defaultLevel, validMinutes: Int = 0, - date: Long = System.currentTimeMillis(), + date: Long = Clock.System.now().toEpochMilliseconds(), validTo: Long = 0L, @RawRes soundRes: Int? = null, actions: List = emptyList(), diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt index 8206fd655b14..f3a1d482b8d2 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt @@ -2,6 +2,7 @@ package app.aaps.core.interfaces.nsclient import kotlinx.serialization.json.JsonElement import java.util.concurrent.atomic.AtomicLong +import kotlin.time.Clock class NSClientLog( val action: String, @@ -9,7 +10,7 @@ class NSClientLog( val json: JsonElement? = null ) { - val date: Long = System.currentTimeMillis() + val date: Long = Clock.System.now().toEpochMilliseconds() val id: Long = idCounter.getAndIncrement() companion object { diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt index 2271f81984a6..fd9ed943f0ba 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt @@ -8,17 +8,18 @@ import app.aaps.core.data.model.ICfg import app.aaps.core.data.model.IDs import app.aaps.core.data.model.TE import app.aaps.core.data.pump.defs.PumpType +import kotlin.time.Clock class DetailedBolusInfo { - val id = System.currentTimeMillis() + val id = Clock.System.now().toEpochMilliseconds() // Requesting parameters for driver @JvmField var insulin = 0.0 @JvmField var carbs = 0.0 // Additional requesting parameters - @JvmField var timestamp = System.currentTimeMillis() + @JvmField var timestamp = Clock.System.now().toEpochMilliseconds() var lastKnownBolusTime: Long = 0 // for SMB check var deliverAtTheLatest: Long = 0 // SMB should be delivered within 1 min from this time @Transient var context: Context? = null // context for progress dialog diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt index 0451e1e5350c..a91fd2bce69c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt @@ -14,6 +14,7 @@ import app.aaps.core.interfaces.utils.DateUtil import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt +import kotlin.time.Clock /** * This interface allows pump drivers to push data changes (creation and update of treatments, temporary basals and extended boluses) back to AAPS-core. @@ -93,7 +94,7 @@ interface PumpSync { ) { val end: Long get() = timestamp + duration - val plannedRemainingMinutes: Long get() = max(T.msecs(end - System.currentTimeMillis()).mins(), 0L) + val plannedRemainingMinutes: Long get() = max(T.msecs(end - Clock.System.now().toEpochMilliseconds()).mins(), 0L) fun convertedToAbsolute(time: Long, profile: Profile): Double = if (isAbsolute) rate else profile.getBasal(time) * rate / 100 @@ -132,7 +133,7 @@ interface PumpSync { get() = timestamp + duration val plannedRemainingMinutes: Long - get() = max(T.msecs(end - System.currentTimeMillis()).mins(), 0L) + get() = max(T.msecs(end - Clock.System.now().toEpochMilliseconds()).mins(), 0L) private fun getPassedDurationToTimeInMinutes(time: Long): Int = ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt index 25686b653f70..a5f3753b41b1 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import java.util.Date import java.util.Objects +import kotlin.time.Clock @Serializable sealed class EventData : Event() { @@ -29,14 +30,14 @@ sealed class EventData : Event() { fun deserialize(json: String) = try { lenientJson.decodeFromString(serializer(), json) } catch (_: Exception) { - Error(System.currentTimeMillis()) + Error(Clock.System.now().toEpochMilliseconds()) } @ExperimentalSerializationApi fun deserializeByte(byteArray: ByteArray) = try { ProtoBuf.decodeFromByteArray(serializer(), byteArray) } catch (_: Exception) { - Error(System.currentTimeMillis()) + Error(Clock.System.now().toEpochMilliseconds()) } } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt index 66c38d69433c..8039430b7b0d 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt @@ -1,6 +1,7 @@ package app.aaps.core.interfaces.smsCommunicator import android.telephony.SmsMessage +import kotlin.time.Clock class Sms { @@ -22,7 +23,7 @@ class Sms { constructor(phoneNumber: String, text: String) { this.phoneNumber = phoneNumber this.text = text - date = System.currentTimeMillis() + date = Clock.System.now().toEpochMilliseconds() sent = true } From d54f5c52c10502aafc824717b859caf307d4e77c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 08:06:17 +0200 Subject: [PATCH 050/146] :pump:virtual delay instead of blocking sleep --- .../app/aaps/pump/virtual/VirtualPumpComposeContent.kt | 2 -- .../kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt | 8 ++++---- .../kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt | 2 -- .../app/aaps/pump/virtual/VirtualPumpViewModelTest.kt | 3 --- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpComposeContent.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpComposeContent.kt index 83d3daa0478c..5813eecd455b 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpComposeContent.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpComposeContent.kt @@ -35,7 +35,6 @@ class VirtualPumpComposeContent( onSettings: (() -> Unit)? ) { val scope = rememberCoroutineScope() - val context = androidx.compose.ui.platform.LocalContext.current val viewModel = remember { VirtualPumpViewModel( virtualPumpPlugin = virtualPumpPlugin, @@ -47,7 +46,6 @@ class VirtualPumpComposeContent( ch = ch, rxBus = rxBus, commandQueue = commandQueue, - context = context, scope = scope ) } diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt index 94e9646f5127..ba624870e958 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt @@ -1,6 +1,6 @@ package app.aaps.pump.virtual -import android.os.SystemClock +import kotlinx.coroutines.delay import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription @@ -218,7 +218,7 @@ open class VirtualPumpPlugin @Inject constructor( var delivering = 0.0 var stopped = false while (delivering < detailedBolusInfo.insulin) { - SystemClock.sleep(200) + delay(200) delivering = (delivering + 0.1).coerceAtMost(detailedBolusInfo.insulin) bolusProgressData.updateProgress(delivered = PumpInsulin(delivering)) if (bolusProgressData.isStopPressed) { @@ -228,9 +228,9 @@ open class VirtualPumpPlugin @Inject constructor( } if (!stopped) { - SystemClock.sleep(200) + delay(200) bolusProgressData.updateProgress(100) - SystemClock.sleep(1000) + delay(1000) } else { result.comment(rh.gs(app.aaps.core.ui.R.string.stop)) } diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt index 80db51f9d0c6..d9bcb8058f64 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpViewModel.kt @@ -1,6 +1,5 @@ package app.aaps.pump.virtual -import android.content.Context import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import app.aaps.core.data.model.EB @@ -50,7 +49,6 @@ class VirtualPumpViewModel( private val ch: ConcentrationHelper, rxBus: RxBus, commandQueue: CommandQueue, - context: Context, scope: CoroutineScope ) { diff --git a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt index 9f0ac24d8a76..2330918fcb9b 100644 --- a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt +++ b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt @@ -1,6 +1,5 @@ package app.aaps.pump.virtual -import android.content.Context import app.aaps.core.data.model.EB import app.aaps.core.data.model.EPS import app.aaps.core.data.model.TB @@ -40,7 +39,6 @@ internal class VirtualPumpViewModelTest { @Mock private lateinit var ch: ConcentrationHelper @Mock private lateinit var rxBus: RxBus @Mock private lateinit var commandQueue: CommandQueue - @Mock private lateinit var context: Context // PumpCommunicationStatus subscribes to these in its init; they never emit so the status // banner / queue stay null and the launched collectors just park. @@ -81,7 +79,6 @@ internal class VirtualPumpViewModelTest { ch = ch, rxBus = rxBus, commandQueue = commandQueue, - context = context, scope = CoroutineScope(Dispatchers.Unconfined) ) From 02c9032a40311f463142f43f8f72b1ce82f3919f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 08:23:43 +0200 Subject: [PATCH 051/146] :core:interfaces remove dead pump layer members --- .../kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt | 2 -- .../app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt | 4 ---- .../kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt index fd9ed943f0ba..d54c796b2ce9 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.pump -import android.content.Context import app.aaps.core.data.model.BCR import app.aaps.core.data.model.BS import app.aaps.core.data.model.CA @@ -22,7 +21,6 @@ class DetailedBolusInfo { @JvmField var timestamp = Clock.System.now().toEpochMilliseconds() var lastKnownBolusTime: Long = 0 // for SMB check var deliverAtTheLatest: Long = 0 // SMB should be delivered within 1 min from this time - @Transient var context: Context? = null // context for progress dialog // Prefilled info for storing to db var bolusCalculatorResult: BCR? = null diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt index 087c68157c41..c0345bc874cc 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt @@ -16,10 +16,6 @@ fun PumpType.determineCorrectBolusSize(bolusAmount: Double): Double = fun PumpType.determineCorrectBolusStepSize(bolusAmount: Double): Double = specialBolusSize()?.getStepSizeForAmount(bolusAmount) ?: bolusSize() -fun PumpType.determineCorrectExtendedBolusSize(bolusAmount: Double): Double { - val ebSettings = extendedBolusSettings() ?: throw IllegalStateException() - return Round.roundTo(min(bolusAmount, ebSettings.maxDose), ebSettings.step) -} fun PumpType.determineCorrectBasalSize(basalAmount: Double): Double { val tSettings = tbrSettings() ?: throw IllegalStateException() diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt index 312a4dfbb68f..115974661d69 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt @@ -1,12 +1,10 @@ package app.aaps.core.interfaces.queue -import java.io.Serializable - /** * Implement this interface for every custom pump command that you want to be able to queue * See [app.aaps.core.interfaces.queue.CommandQueue.customCommand] for queuing a custom command. */ -interface CustomCommand : Serializable { +interface CustomCommand { /** * @return short description of this command to be used in [app.aaps.core.interfaces.queue.Command.status] From 6eb7f8c0b18e9e078b6fbe5fa9243ef00c4d0bc2 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 14:00:26 +0200 Subject: [PATCH 052/146] :core:utils is multiplatform --- core/utils/build.gradle.kts | 112 +++++++++++++----- .../app/aaps/core/utils/JsonHelperTest.kt | 0 .../app/aaps/core/utils/MidnightUtilsTest.kt | 0 .../app/aaps/core/utils/PercentileTest.kt | 0 .../core/utils/receivers/StringUtilsTest.kt | 0 .../pump/common/utils/DateTimeUtilUTest.kt | 0 .../{main => androidMain}/AndroidManifest.xml | 0 .../kotlin/app/aaps/core/utils/Concurrency.kt | 0 .../app/aaps/core/utils/DateTimeUtil.kt | 0 .../core/utils/DeferredForegroundStart.kt | 0 .../app/aaps/core/utils/EspressoTestHelper.kt | 0 .../kotlin/app/aaps/core/utils/HtmlHelper.kt | 0 .../kotlin/app/aaps/core/utils/JsonHelper.kt | 0 .../kotlin/app/aaps/core/utils/StringUtil.kt | 0 .../extensions/BluetoothAdapterExtension.kt | 0 .../extensions/BluetoothDeviceExtension.kt | 0 .../core/utils/extensions/IntentExtension.kt | 0 .../utils/extensions/WorkerDataBuilder.kt | 0 .../app/aaps/core/utils/fabric/InstanceId.kt | 0 .../app/aaps/core/utils/pump/ByteUtil.kt | 0 .../app/aaps/core/utils/pump/ThreadUtil.kt | 0 .../aaps/core/utils/receivers/BundleLogger.kt | 0 .../core/utils/receivers/DataWorkerStorage.kt | 0 .../app/aaps/core/utils/receivers/Inbox.kt | 0 .../aaps/core/utils/worker/WorkExtensions.kt | 0 .../aaps/core/utils/HexByteArrayConversion.kt | 14 ++- .../app/aaps/core/utils/MidnightUtils.kt | 61 ++++++++++ .../kotlin/app/aaps/core/utils/Percentile.kt | 0 .../aaps/core/utils/receivers/StringUtils.kt | 0 .../app/aaps/core/utils/MidnightUtils.kt | 56 --------- 30 files changed, 152 insertions(+), 91 deletions(-) rename core/utils/src/{test => androidHostTest}/kotlin/app/aaps/core/utils/JsonHelperTest.kt (100%) rename core/utils/src/{test => androidHostTest}/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt (100%) rename core/utils/src/{test => androidHostTest}/kotlin/app/aaps/core/utils/PercentileTest.kt (100%) rename core/utils/src/{test => androidHostTest}/kotlin/app/aaps/core/utils/receivers/StringUtilsTest.kt (100%) rename core/utils/src/{test => androidHostTest}/kotlin/app/aaps/pump/common/utils/DateTimeUtilUTest.kt (100%) rename core/utils/src/{main => androidMain}/AndroidManifest.xml (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/Concurrency.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/DateTimeUtil.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/DeferredForegroundStart.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/EspressoTestHelper.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/HtmlHelper.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/JsonHelper.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/StringUtil.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/extensions/BluetoothAdapterExtension.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/extensions/BluetoothDeviceExtension.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/extensions/IntentExtension.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/extensions/WorkerDataBuilder.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/fabric/InstanceId.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/pump/ByteUtil.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/pump/ThreadUtil.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/receivers/BundleLogger.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/receivers/DataWorkerStorage.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/receivers/Inbox.kt (100%) rename core/utils/src/{main => androidMain}/kotlin/app/aaps/core/utils/worker/WorkExtensions.kt (100%) rename core/utils/src/{main => commonMain}/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt (61%) create mode 100644 core/utils/src/commonMain/kotlin/app/aaps/core/utils/MidnightUtils.kt rename core/utils/src/{main => commonMain}/kotlin/app/aaps/core/utils/Percentile.kt (100%) rename core/utils/src/{main => commonMain}/kotlin/app/aaps/core/utils/receivers/StringUtils.kt (100%) delete mode 100644 core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index b908c8861eb4..f2a8b74724a9 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -1,42 +1,96 @@ import kotlin.math.min plugins { - alias(libs.plugins.android.library) - kotlin("plugin.allopen") - id("android-module-dependencies") - id("all-open-dependencies") - id("test-module-dependencies") - id("jacoco-module-dependencies") + kotlin("multiplatform") + // NOT com.android.library. AGP 9 refuses that plugin together with the multiplatform plugin. + // Same reason as :core:keys and :core:data. + alias(libs.plugins.android.kmp.library) } -android { - namespace = "app.aaps.core.utils" - defaultConfig { - minSdk = min(Versions.minSdk, Versions.wearMinSdk) +// The first module SPLIT rather than converted whole. Most of what lives here is Android glue - +// WorkManager, Bluetooth, Intent/Bundle, Html - and only a handful of files are genuinely portable, +// so androidMain keeps the bulk and commonMain takes the four that carry no platform types. +// +// The `allopen` plugin is deliberately not carried over: it exists to open @OpenForTesting classes +// for mocking and nothing in this module is annotated with it. +kotlin { + android { + namespace = "app.aaps.core.utils" + compileSdk = Versions.compileSdk + minSdk = min(Versions.minSdk, Versions.wearMinSdk) // Compatible with wear module + // Creates the androidHostTest compilation, which also pulls in commonTest. + withHostTest { } + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + // Restated from android-module-dependencies, which a multiplatform module cannot apply. + lint { + checkReleaseBuilds = false + disable += "MissingTranslation" + disable += "ExtraTranslation" + } } -} -dependencies { + jvm { + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + } + + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.datetime) + } + } + + androidMain { + dependencies { + // Everything below was `api` on the old android library and several of the 24 + // consumer modules resolve these transitively, so they must stay exported. They are + // JVM/Android only, which is exactly why they belong to this source set. + api(libs.net.danlew.android.joda) + api(project.dependencies.platform(libs.kotlinx.serialization.bom)) + api(libs.kotlinx.serialization.json) - api(libs.net.danlew.android.joda) - api(platform(libs.kotlinx.serialization.bom)) - api(libs.kotlinx.serialization.json) + //Firebase + api(project.dependencies.platform(libs.com.google.firebase.bom)) + api(libs.com.google.firebase.analytics) + api(libs.com.google.firebase.crashlytics) - //Firebase - api(platform(libs.com.google.firebase.bom)) - api(libs.com.google.firebase.analytics) - api(libs.com.google.firebase.crashlytics) + //CryptoUtil + api(libs.com.madgag.spongycastle) + api(libs.com.google.crypto.tink) - //CryptoUtil - api(libs.com.madgag.spongycastle) - api(libs.com.google.crypto.tink) + //WorkManager + api(libs.androidx.work.runtime) // DataWorkerStorage - //WorkManager - api(libs.androidx.work.runtime) // DataWorkerStorage + // ProcessLifecycleOwner for DeferredForegroundStart + implementation(libs.androidx.lifecycle.process) - // ProcessLifecycleOwner for DeferredForegroundStart - implementation(libs.androidx.lifecycle.process) + api(libs.com.google.dagger.android) // for javax.inject annotations + api(libs.com.google.dagger.android.support) + } + } - api(libs.com.google.dagger.android) // for javax.inject annotations - api(libs.com.google.dagger.android.support) -} \ No newline at end of file + // Hand written rather than taken from test-module-dependencies, which applies + // com.android.library and so cannot be used here. Same approach as :core:keys. + getByName("androidHostTest") { + dependencies { + implementation(libs.org.junit.jupiter) + implementation(libs.org.junit.jupiter.api) + implementation(libs.com.google.truth) + implementation(libs.org.mockito.kotlin) + // JsonHelperTest needs a REAL org.json. On the Android unit-test classpath the + // platform one is a stub that throws "not mocked"; test-module-dependencies used to + // supply the real thing transitively through jsonassert, and this module can no + // longer apply that plugin. This is AOSP's own implementation, repackaged. + implementation(libs.org.json.android) + runtimeOnly(libs.org.junit.platform.launcher) + } + } + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/JsonHelperTest.kt b/core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/JsonHelperTest.kt similarity index 100% rename from core/utils/src/test/kotlin/app/aaps/core/utils/JsonHelperTest.kt rename to core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/JsonHelperTest.kt diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt b/core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt similarity index 100% rename from core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt rename to core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/PercentileTest.kt b/core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/PercentileTest.kt similarity index 100% rename from core/utils/src/test/kotlin/app/aaps/core/utils/PercentileTest.kt rename to core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/PercentileTest.kt diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/receivers/StringUtilsTest.kt b/core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/receivers/StringUtilsTest.kt similarity index 100% rename from core/utils/src/test/kotlin/app/aaps/core/utils/receivers/StringUtilsTest.kt rename to core/utils/src/androidHostTest/kotlin/app/aaps/core/utils/receivers/StringUtilsTest.kt diff --git a/core/utils/src/test/kotlin/app/aaps/pump/common/utils/DateTimeUtilUTest.kt b/core/utils/src/androidHostTest/kotlin/app/aaps/pump/common/utils/DateTimeUtilUTest.kt similarity index 100% rename from core/utils/src/test/kotlin/app/aaps/pump/common/utils/DateTimeUtilUTest.kt rename to core/utils/src/androidHostTest/kotlin/app/aaps/pump/common/utils/DateTimeUtilUTest.kt diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/androidMain/AndroidManifest.xml similarity index 100% rename from core/utils/src/main/AndroidManifest.xml rename to core/utils/src/androidMain/AndroidManifest.xml diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/Concurrency.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/Concurrency.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/Concurrency.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/Concurrency.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/DateTimeUtil.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/DateTimeUtil.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/DateTimeUtil.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/DateTimeUtil.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/DeferredForegroundStart.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/DeferredForegroundStart.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/DeferredForegroundStart.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/DeferredForegroundStart.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/EspressoTestHelper.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/EspressoTestHelper.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/EspressoTestHelper.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/EspressoTestHelper.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/HtmlHelper.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/HtmlHelper.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/JsonHelper.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/JsonHelper.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/JsonHelper.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/JsonHelper.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/StringUtil.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/StringUtil.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/StringUtil.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/StringUtil.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/extensions/BluetoothAdapterExtension.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/BluetoothAdapterExtension.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/extensions/BluetoothAdapterExtension.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/BluetoothAdapterExtension.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/extensions/BluetoothDeviceExtension.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/BluetoothDeviceExtension.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/extensions/BluetoothDeviceExtension.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/BluetoothDeviceExtension.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/extensions/IntentExtension.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/IntentExtension.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/extensions/IntentExtension.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/IntentExtension.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/extensions/WorkerDataBuilder.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/WorkerDataBuilder.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/extensions/WorkerDataBuilder.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/extensions/WorkerDataBuilder.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/fabric/InstanceId.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/fabric/InstanceId.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/fabric/InstanceId.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/fabric/InstanceId.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/pump/ByteUtil.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/pump/ByteUtil.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/pump/ByteUtil.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/pump/ByteUtil.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/pump/ThreadUtil.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/pump/ThreadUtil.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/pump/ThreadUtil.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/pump/ThreadUtil.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/receivers/BundleLogger.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/BundleLogger.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/receivers/BundleLogger.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/BundleLogger.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/receivers/DataWorkerStorage.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/DataWorkerStorage.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/receivers/DataWorkerStorage.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/DataWorkerStorage.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/receivers/Inbox.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/Inbox.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/receivers/Inbox.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/receivers/Inbox.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/worker/WorkExtensions.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/worker/WorkExtensions.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/worker/WorkExtensions.kt rename to core/utils/src/androidMain/kotlin/app/aaps/core/utils/worker/WorkExtensions.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt similarity index 61% rename from core/utils/src/main/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt rename to core/utils/src/commonMain/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt index 58e6f6235fad..928b1265394e 100644 --- a/core/utils/src/main/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt +++ b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/HexByteArrayConversion.kt @@ -1,11 +1,11 @@ package app.aaps.core.utils -import java.util.Locale - private val HEX_CHARS = "0123456789abcdef".toCharArray() -fun ByteArray.toHex() : String{ - val result = StringBuffer() +fun ByteArray.toHex(): String { + // StringBuilder, not StringBuffer: StringBuffer is a JVM class and its synchronization bought + // nothing here - the builder never leaves this function. + val result = StringBuilder() forEach { val octet = it.toInt() @@ -22,7 +22,9 @@ fun String.hexStringToByteArray(): ByteArray { val result = ByteArray(length / 2) - val lowerCased = this.lowercase(Locale.getDefault()) + // Locale independent lowercase. The previous `lowercase(Locale.getDefault())` made hex parsing + // depend on the phone's language, which is never what a wire format wants. + val lowerCased = this.lowercase() for (i in indices step 2) { val firstIndex = HEX_CHARS.indexOf(lowerCased[i]) val secondIndex = HEX_CHARS.indexOf(lowerCased[i + 1]) @@ -32,4 +34,4 @@ fun String.hexStringToByteArray(): ByteArray { } return result -} \ No newline at end of file +} diff --git a/core/utils/src/commonMain/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/MidnightUtils.kt new file mode 100644 index 000000000000..1685adb6c4a8 --- /dev/null +++ b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/MidnightUtils.kt @@ -0,0 +1,61 @@ +package app.aaps.core.utils + +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.Instant + +/** + * Midnight time conversion + */ +object MidnightUtils { + + /** + * The wall clock offset of the day's first instant. + * + * Normally 00:00, so this is zero and every result is simply the time of day. On a day where the + * clocks jump forward over midnight (Brazil used to do this) local midnight does not exist, and + * `atStartOfDayIn` gives 01:00 instead - the same thing `atStartOfDay(zone)` did before. Keeping + * the subtraction preserves the old "ignoring DST change" behaviour exactly rather than assuming + * the day starts at zero. + */ + private fun secondsFrom(timestamp: Long, tz: TimeZone): Int { + val local = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(tz) + val startOfDay = local.date.atStartOfDayIn(tz).toLocalDateTime(tz) + return local.time.toSecondOfDay() - startOfDay.time.toSecondOfDay() + } + + /** + * Actual passed seconds from midnight ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @return seconds + */ + fun secondsFromMidnight(): Int = + secondsFrom(Clock.System.now().toEpochMilliseconds(), TimeZone.currentSystemDefault()) + + /** + * Passed seconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return seconds + */ + fun secondsFromMidnight(timestamp: Long): Int = + secondsFrom(timestamp, TimeZone.currentSystemDefault()) + + /** + * Passed milliseconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return milliseconds + */ + fun milliSecFromMidnight(timestamp: Long): Long { + val tz = TimeZone.currentSystemDefault() + val local = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(tz) + val startOfDay = local.date.atStartOfDayIn(tz).toLocalDateTime(tz) + return (local.time.toMillisecondOfDay() - startOfDay.time.toMillisecondOfDay()).toLong() + } +} diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/Percentile.kt b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/Percentile.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/Percentile.kt rename to core/utils/src/commonMain/kotlin/app/aaps/core/utils/Percentile.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/receivers/StringUtils.kt b/core/utils/src/commonMain/kotlin/app/aaps/core/utils/receivers/StringUtils.kt similarity index 100% rename from core/utils/src/main/kotlin/app/aaps/core/utils/receivers/StringUtils.kt rename to core/utils/src/commonMain/kotlin/app/aaps/core/utils/receivers/StringUtils.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt deleted file mode 100644 index 49b808f125e8..000000000000 --- a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt +++ /dev/null @@ -1,56 +0,0 @@ -package app.aaps.core.utils - -import java.time.Duration -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime - -/** - * Midnight time conversion - */ -object MidnightUtils { - - /** - * Actual passed seconds from midnight ignoring DST change - * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) - * - * @return seconds - */ - fun secondsFromMidnight(): Int { - val nowZoned = ZonedDateTime.now() - val localTime = nowZoned.toLocalTime() - val midnight = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toLocalTime() - val duration = Duration.between(midnight, localTime) - return duration.seconds.toInt() - } - - /** - * Passed seconds from midnight for specified time ignoring DST change - * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) - * - * @param timestamp time - * @return seconds - */ - fun secondsFromMidnight(timestamp: Long): Int { - val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) - val localTime = timeZoned.toLocalTime() - val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() - val duration: Duration = Duration.between(midnight, localTime) - return duration.seconds.toInt() - } - - /** - * Passed milliseconds from midnight for specified time ignoring DST change - * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) - * - * @param timestamp time - * @return milliseconds - */ - fun milliSecFromMidnight(timestamp: Long): Long { - val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) - val localTime = timeZoned.toLocalTime() - val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() - val duration = Duration.between(midnight, localTime) - return duration.toMillis() - } -} \ No newline at end of file From f0142bc9f5d9f3d21090f9e40f445e1864a9f1a8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 19:50:04 +0200 Subject: [PATCH 053/146] :core:interfaces TextRef for notification and permission text --- .../kotlin/app/aaps/ComposeMainActivity.kt | 8 ++++---- app/src/main/kotlin/app/aaps/MainApp.kt | 7 ++++--- .../maintenance/CloudStorageProvider.kt | 9 +++++---- .../notifications/NotificationAction.kt | 4 ++-- .../core/interfaces/plugin/PermissionGroup.kt | 6 +++--- .../interfaces/protection/ProtectionCheck.kt | 4 ++-- .../core/interfaces/pump/PumpPluginBase.kt | 5 +++-- .../app/aaps/core/ui/compose/ProtectionHost.kt | 18 +++++++++++------- .../cloud/CloudDirectoryManagerImpl.kt | 4 ++-- .../googledrive/GoogleDriveProvider.kt | 5 +++-- .../aaps/implementation/plugin/PluginStore.kt | 13 +++++++------ .../protection/BiometricCheck.kt | 8 ++++---- .../protection/ProtectionCheckImpl.kt | 3 ++- .../implementation/plugin/PluginStoreTest.kt | 5 +++-- .../plugins/automation/AutomationRuntime.kt | 9 +++++---- .../calibration/LinearCalibrationPlugin.kt | 3 ++- .../setupwizard/elements/SWPermissions.kt | 1 + .../constraints/dstHelper/DstHelperPlugin.kt | 5 +++-- .../app/aaps/plugins/source/DexcomPlugin.kt | 5 +++-- .../plugins/source/NotificationReaderPlugin.kt | 5 +++-- .../sync/nsclientV3/NsIncomingDataProcessor.kt | 3 ++- .../nsclientV3/services/NSClientV3Service.kt | 3 ++- .../openhumans/OpenHumansUploaderPlugin.kt | 3 ++- .../smsCommunicator/SmsCommunicatorPlugin.kt | 5 +++-- .../app/aaps/pump/eopatch/EopatchPumpPlugin.kt | 4 ++-- .../aaps/pump/eopatch/alarm/AlarmManager.kt | 13 ++++++++----- .../NotificationBottomSheet.kt | 10 ++++++---- .../permissionsSheet/PermissionsSheet.kt | 1 + .../PermissionsSheetPreviews.kt | 13 +++++++------ .../PermissionsSheetContentTest.kt | 5 +++-- 30 files changed, 108 insertions(+), 79 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt index 7e9c8086c7d0..e0dd6ae74c41 100644 --- a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt +++ b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt @@ -497,11 +497,11 @@ class ComposeMainActivity : AppCompatActivity() { protectionCheck = protectionCheck, preferences = preferences, checkPassword = cryptoUtil::checkPassword, - showBiometric = { activity, titleRes, onGranted, onCancelled, onDenied -> - BiometricCheck.biometricPrompt(activity, titleRes, rxBus, onGranted, onCancelled, onDenied, passwordCheck) + showBiometric = { activity, title, onGranted, onCancelled, onDenied -> + BiometricCheck.biometricPrompt(activity, title, rxBus, onGranted, onCancelled, onDenied, passwordCheck) }, - showBiometricSimple = { activity, titleRes, onSuccess, onFallback, onCancel -> - BiometricCheck.biometricPromptSimple(activity, titleRes, rxBus, onSuccess, onFallback, onCancel) + showBiometricSimple = { activity, title, onSuccess, onFallback, onCancel -> + BiometricCheck.biometricPromptSimple(activity, title, rxBus, onSuccess, onFallback, onCancel) } ) diff --git a/app/src/main/kotlin/app/aaps/MainApp.kt b/app/src/main/kotlin/app/aaps/MainApp.kt index 356ed9048cd7..fcf4f3a246b5 100644 --- a/app/src/main/kotlin/app/aaps/MainApp.kt +++ b/app/src/main/kotlin/app/aaps/MainApp.kt @@ -73,6 +73,7 @@ import app.aaps.core.keys.StringKey import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.crypto.CryptoUtil import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.locale.LocaleHelper @@ -471,7 +472,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { id = NotificationId.IDENTIFICATION_NOT_SET, R.string.identification_not_set, level = NotificationLevel.INFO, - actions = listOf(NotificationAction(R.string.set) {}), + actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.set)) {}), validityCheck = { config.isDev() && preferences.get(StringKey.MaintenanceIdentification).isBlank() } ) // Master password not set @@ -480,7 +481,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { id = NotificationId.MASTER_PASSWORD_NOT_SET, app.aaps.core.ui.R.string.master_password_not_set, level = NotificationLevel.NORMAL, - actions = listOf(NotificationAction(R.string.set) {}), + actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.set)) {}), validityCheck = { preferences.get(StringKey.ProtectionMasterPassword) == "" } ) // AAPS directory not selected @@ -489,7 +490,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { id = NotificationId.AAPS_DIR_NOT_SELECTED, app.aaps.core.ui.R.string.aaps_directory_not_selected, level = NotificationLevel.LOW, - actions = listOf(NotificationAction(R.string.select) {}), + actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.select)) {}), validityCheck = { preferences.getIfExists(StringKey.AapsDirectoryUri).isNullOrEmpty() } ) } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt index b6405a3b18c7..9a50a6c2f67a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt @@ -1,6 +1,7 @@ package app.aaps.core.interfaces.maintenance import androidx.compose.ui.graphics.vector.ImageVector +import app.aaps.core.keys.interfaces.TextRef /** * Abstract interface for cloud storage providers. @@ -31,14 +32,14 @@ interface CloudStorageProvider { val icon: ImageVector /** - * String resource ID for "authorized" status text (e.g., "Google Drive Authorized") + * Text for "authorized" status (e.g., "Google Drive Authorized") */ - val authorizedTextResId: Int + val authorizedText: TextRef /** - * String resource ID for "re-authorization required" status text + * Text for "re-authorization required" status */ - val reAuthRequiredTextResId: Int + val reAuthRequiredText: TextRef // ==================== Authentication ==================== diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt index 5a475000cc46..5ee0d5799ac3 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt @@ -1,8 +1,8 @@ package app.aaps.core.interfaces.notifications -import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef data class NotificationAction( - @StringRes val buttonTextRes: Int, + val buttonText: TextRef, val action: () -> Unit ) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt index 9a9d58a36668..5eba9a88712a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.plugin -import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef /** * Declares a group of related runtime permissions that a plugin requires. @@ -17,8 +17,8 @@ import androidx.annotation.StringRes */ data class PermissionGroup( val permissions: List, - @StringRes val rationaleTitle: Int, - @StringRes val rationaleDescription: Int, + val rationaleTitle: TextRef, + val rationaleDescription: TextRef, val special: Boolean = false, val alwaysShowAction: Boolean = false, ) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt index a690a51a9492..cc08491850f6 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.protection -import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef import kotlinx.coroutines.flow.StateFlow /** @@ -61,7 +61,7 @@ data class ProtectionRequest( val id: Long, val protection: ProtectionCheck.Protection, val type: ProtectionType, - @StringRes val titleRes: Int, + val title: TextRef, val onResult: (ProtectionResult) -> Unit ) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt index 4a2be0f909a1..b9bfc7ce44ba 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt @@ -14,6 +14,7 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.NonPreferenceKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -62,8 +63,8 @@ abstract class PumpPluginBase( override fun requiredPermissions(): List = listOf( PermissionGroup( permissions = listOf(Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN), - rationaleTitle = R.string.permission_bluetooth_title, - rationaleDescription = R.string.permission_bluetooth_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_bluetooth_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_bluetooth_description), ) ) } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt index 88c9dd4b3d0b..85b0d9bf178b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt @@ -15,6 +15,7 @@ import app.aaps.core.interfaces.protection.AuthorizationResult import app.aaps.core.interfaces.protection.ProtectionCheck import app.aaps.core.interfaces.protection.ProtectionResult import app.aaps.core.interfaces.protection.ProtectionType +import app.aaps.core.ui.compose.stringResource import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.dialogs.QueryPasswordDialog @@ -38,8 +39,8 @@ fun ProtectionHost( protectionCheck: ProtectionCheck, preferences: Preferences, checkPassword: (password: String, hash: String) -> Boolean, - showBiometric: (FragmentActivity, Int, () -> Unit, () -> Unit, () -> Unit) -> Unit, - showBiometricSimple: (FragmentActivity, Int, () -> Unit, () -> Unit, () -> Unit) -> Unit = showBiometric + showBiometric: (FragmentActivity, String, () -> Unit, () -> Unit, () -> Unit) -> Unit, + showBiometricSimple: (FragmentActivity, String, () -> Unit, () -> Unit, () -> Unit) -> Unit = showBiometric ) { val context = LocalContext.current val activity = context as? FragmentActivity @@ -49,13 +50,15 @@ fun ProtectionHost( authRequest?.let { req -> var showDialog by remember(req.id) { mutableStateOf(!req.hasBiometric) } + // Read while still in composition: stringResource is @Composable, the biometric callback is not. + val biometricTitle = stringResource(app.aaps.core.ui.R.string.biometric_title) if (req.hasBiometric && !showDialog) { LaunchedEffect(req.id) { if (activity != null) { showBiometricSimple( activity, - app.aaps.core.ui.R.string.biometric_title, + biometricTitle, { // Biometric success → grant highest level that uses biometric protectionCheck.completeAuthRequest( @@ -94,6 +97,7 @@ fun ProtectionHost( val request by protectionCheck.pendingRequest.collectAsStateWithLifecycle() request?.let { req -> + val reqTitle = stringResource(req.title) when (req.type) { ProtectionType.NONE -> { LaunchedEffect(req.id) { @@ -106,7 +110,7 @@ fun ProtectionHost( if (activity != null) { showBiometric( activity, - req.titleRes, + reqTitle, { protectionCheck.completeRequest(req.id, ProtectionResult.GRANTED) }, { protectionCheck.completeRequest(req.id, ProtectionResult.CANCELLED) }, { protectionCheck.completeRequest(req.id, ProtectionResult.DENIED) } @@ -120,7 +124,7 @@ fun ProtectionHost( ProtectionType.MASTER_PASSWORD -> { val storedHash = preferences.get(StringKey.ProtectionMasterPassword) QueryPasswordDialog( - title = stringResource(req.titleRes), + title = stringResource(req.title), pinInput = false, onConfirm = { enteredPassword -> if (checkPassword(enteredPassword, storedHash)) { @@ -142,7 +146,7 @@ fun ProtectionHost( } val storedHash = preferences.get(passwordKey) QueryPasswordDialog( - title = stringResource(req.titleRes), + title = stringResource(req.title), pinInput = false, onConfirm = { enteredPassword -> if (checkPassword(enteredPassword, storedHash)) { @@ -164,7 +168,7 @@ fun ProtectionHost( } val storedHash = preferences.get(pinKey) QueryPasswordDialog( - title = stringResource(req.titleRes), + title = stringResource(req.title), pinInput = true, onConfirm = { enteredPin -> if (checkPassword(enteredPin, storedHash)) { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/CloudDirectoryManagerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/CloudDirectoryManagerImpl.kt index f43d8d319439..4d0b935f786a 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/CloudDirectoryManagerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/CloudDirectoryManagerImpl.kt @@ -28,8 +28,8 @@ class CloudDirectoryManagerImpl @Inject constructor( val authorizedStatusText = when { !hasCredentials -> "" - hasConnectionError -> provider.let { rh.gs(it.reAuthRequiredTextResId) } - else -> provider.let { rh.gs(it.authorizedTextResId) } + hasConnectionError -> provider.let { rh.gs(it.reAuthRequiredText) } + else -> provider.let { rh.gs(it.authorizedText) } } return CloudDirectoryInfo( diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/providers/googledrive/GoogleDriveProvider.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/providers/googledrive/GoogleDriveProvider.kt index d4fa0603997e..148f59a1e844 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/providers/googledrive/GoogleDriveProvider.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/cloud/providers/googledrive/GoogleDriveProvider.kt @@ -8,6 +8,7 @@ import app.aaps.core.interfaces.maintenance.CloudFileListResult import app.aaps.core.interfaces.maintenance.CloudFolder import app.aaps.core.interfaces.maintenance.CloudStorageProvider import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.icons.IcGoogleDrive import app.aaps.implementation.R import app.aaps.implementation.maintenance.cloud.StorageTypes @@ -45,9 +46,9 @@ class GoogleDriveProvider @Inject constructor( override val icon: ImageVector = IcGoogleDrive - override val authorizedTextResId: Int = R.string.google_drive_authorized + override val authorizedText: TextRef = TextRef.AndroidRes(R.string.google_drive_authorized) - override val reAuthRequiredTextResId: Int = R.string.google_drive_reauth_required + override val reAuthRequiredText: TextRef = TextRef.AndroidRes(R.string.google_drive_reauth_required) // ==================== Authentication ==================== diff --git a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt index 7cd8604d4b8b..0634bc85a1c0 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt @@ -32,6 +32,7 @@ import app.aaps.core.interfaces.source.BgSource import app.aaps.core.interfaces.sync.Sync import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.implementation.R import dagger.Lazy import javax.inject.Inject @@ -73,16 +74,16 @@ class PluginStore @Inject constructor( add( PermissionGroup( permissions = listOf(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS), - rationaleTitle = R.string.permission_battery_title, - rationaleDescription = R.string.permission_battery_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_battery_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_battery_description), special = true, ) ) add( PermissionGroup( permissions = listOf(PERMISSION_SELECT_DIRECTORY), - rationaleTitle = R.string.permission_directory_title, - rationaleDescription = R.string.permission_directory_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_directory_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_directory_description), special = true, alwaysShowAction = true, ) @@ -95,8 +96,8 @@ class PluginStore @Inject constructor( add( PermissionGroup( permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), - rationaleTitle = R.string.permission_notifications_title, - rationaleDescription = R.string.permission_notifications_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_notifications_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_notifications_description), special = needsSettingsWorkaround, ) ) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt index 198a0c6a31bc..6307d69157ff 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/BiometricCheck.kt @@ -32,7 +32,7 @@ object BiometricCheck { * All errors and the negative button trigger [onFallback], letting the caller * (e.g. ProtectionHost) show the unified auth dialog instead. */ - fun biometricPromptSimple(activity: FragmentActivity, title: Int, rxBus: RxBus, onSuccess: Runnable?, onFallback: Runnable?, onCancel: Runnable?) { + fun biometricPromptSimple(activity: FragmentActivity, title: String, rxBus: RxBus, onSuccess: Runnable?, onFallback: Runnable?, onCancel: Runnable?) { val executor = ContextCompat.getMainExecutor(activity) val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() { @@ -57,7 +57,7 @@ object BiometricCheck { }) val promptInfo = PromptInfo.Builder() - .setTitle(activity.getString(title)) + .setTitle(title) .setDescription(activity.getString(R.string.biometric_title)) .setNegativeButtonText(activity.getString(R.string.use_pin_password)) .setConfirmationRequired(false) @@ -68,7 +68,7 @@ object BiometricCheck { } } - fun biometricPrompt(activity: FragmentActivity, title: Int, rxBus: RxBus, ok: Runnable?, cancel: Runnable? = null, fail: Runnable? = null, passwordCheck: PasswordCheck) { + fun biometricPrompt(activity: FragmentActivity, title: String, rxBus: RxBus, ok: Runnable?, cancel: Runnable? = null, fail: Runnable? = null, passwordCheck: PasswordCheck) { val executor = ContextCompat.getMainExecutor(activity) val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() { @@ -119,7 +119,7 @@ object BiometricCheck { }) val promptInfo = PromptInfo.Builder() - .setTitle(activity.getString(title)) + .setTitle(title) .setDescription(activity.getString(R.string.biometric_title)) .setNegativeButtonText(activity.getString(R.string.cancel)) // not possible with setDeviceCredentialAllowed .setConfirmationRequired(false) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/protection/ProtectionCheckImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/protection/ProtectionCheckImpl.kt index 05a5ab40016b..c18dfb37278b 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/protection/ProtectionCheckImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/protection/ProtectionCheckImpl.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.IntKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import dagger.Reusable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -291,7 +292,7 @@ class ProtectionCheckImpl @Inject constructor( id = requestIdCounter.incrementAndGet(), protection = protection, type = type, - titleRes = titleRes, + title = TextRef.AndroidRes(titleRes), onResult = { result -> if (result == ProtectionResult.GRANTED) onGranted(protection) _pendingRequest.value = null diff --git a/implementation/src/test/kotlin/app/aaps/implementation/plugin/PluginStoreTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/plugin/PluginStoreTest.kt index 21fa5b788a30..cbafcb181b2a 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/plugin/PluginStoreTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/plugin/PluginStoreTest.kt @@ -6,6 +6,7 @@ import app.aaps.core.interfaces.plugin.PermissionProvider import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.pump.PumpWithConcentration import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat import dagger.Lazy @@ -29,8 +30,8 @@ class PluginStoreTest : TestBase() { private val locationGroup = PermissionGroup( permissions = listOf("android.permission.ACCESS_FINE_LOCATION"), - rationaleTitle = 0, - rationaleDescription = 0 + rationaleTitle = TextRef.Literal(""), + rationaleDescription = TextRef.Literal("") ) private fun store(providers: Set): PluginStore { diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt index a7d52478a5af..2cb42b027863 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt @@ -32,6 +32,7 @@ import app.aaps.core.keys.LongComposedKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.ComposablePluginContent import app.aaps.core.utils.DeferredForegroundStart import app.aaps.plugins.automation.actions.Action @@ -259,13 +260,13 @@ class AutomationRuntime @Inject constructor( if (config.APS && usesLocationTrigger()) listOf( PermissionGroup( permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), - rationaleTitle = R.string.permission_location_title, - rationaleDescription = R.string.permission_location_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_location_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_location_description), ), PermissionGroup( permissions = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), - rationaleTitle = R.string.permission_location_title, - rationaleDescription = R.string.permission_background_location_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_location_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_background_location_description), ), ) else emptyList() diff --git a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt index 1814e508e810..c0f19f2e5c79 100644 --- a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt +++ b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt @@ -26,6 +26,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventCalibrationChanged import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.icons.IcCalibration import app.aaps.plugins.calibration.compose.CalibrationComposeContent import kotlinx.coroutines.CoroutineScope @@ -212,7 +213,7 @@ class LinearCalibrationPlugin @Inject constructor( id = NotificationId.SENSOR_CHANGE_DETECTED, text = rh.gs(R.string.sensor_change_detected_text, dateUtil.timeString(detectedAt)), actions = listOf( - NotificationAction(R.string.sensor_change_detected_action) { + NotificationAction(TextRef.AndroidRes(R.string.sensor_change_detected_action)) { runBlocking { insertSensorChange(detectedAt) } } ) diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWPermissions.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWPermissions.kt index 4eb977b8eddf..da0311ff4742 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWPermissions.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWPermissions.kt @@ -27,6 +27,7 @@ import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.ui.compose.stringResource import app.aaps.plugins.configuration.setupwizard.SWDefinition import javax.inject.Inject diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt index 14ddbfb4402b..847a38622e68 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt @@ -18,6 +18,7 @@ import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.constraints.R import app.aaps.plugins.constraints.dstHelper.keys.DstHelperLongKey import kotlinx.coroutines.runBlocking @@ -61,7 +62,7 @@ class DstHelperPlugin @Inject constructor( notificationManager.post( NotificationId.DST_IN_24H, R.string.dst_in_24h_warning, - actions = listOf(NotificationAction(app.aaps.core.ui.R.string.snooze) { + actions = listOf(NotificationAction(TextRef.AndroidRes(app.aaps.core.ui.R.string.snooze)) { preferences.put(DstHelperLongKey.SnoozeDstIn24h, System.currentTimeMillis() + T.hours(24).msecs()) }) ) @@ -81,7 +82,7 @@ class DstHelperPlugin @Inject constructor( notificationManager.post( NotificationId.DST_LOOP_DISABLED, R.string.dst_loop_disabled_warning, - actions = listOf(NotificationAction(app.aaps.core.ui.R.string.snooze) { + actions = listOf(NotificationAction(TextRef.AndroidRes(app.aaps.core.ui.R.string.snooze)) { preferences.put(DstHelperLongKey.SnoozeLoopDisabled, System.currentTimeMillis() + T.hours(24).msecs()) }) ) diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt index a71776f29f90..3cb7a4ac5033 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt @@ -30,6 +30,7 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.workflow.LoggingWorker import app.aaps.core.ui.compose.icons.IcPluginByoda import app.aaps.core.utils.receivers.DataInbox @@ -79,8 +80,8 @@ class DexcomPlugin @Inject constructor( if (isDexcomAppInstalled()) listOf( PermissionGroup( permissions = listOf(PERMISSION), - rationaleTitle = R.string.permission_dexcom_title, - rationaleDescription = R.string.permission_dexcom_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_dexcom_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_dexcom_description), special = true, ) ) else emptyList() diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt index 0abddc7b83d9..6a60a0aec8df 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt @@ -11,6 +11,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.source.BgSource import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.icons.IcPluginByoda import app.aaps.plugins.source.compose.BgSourceComposeContent import app.aaps.plugins.source.notificationreader.PackageConfig @@ -54,8 +55,8 @@ class NotificationReaderPlugin @Inject constructor( override fun requiredPermissions(): List = listOf( PermissionGroup( permissions = listOf(PERMISSION_NOTIFICATION_LISTENER), - rationaleTitle = R.string.permission_notification_listener_title, - rationaleDescription = R.string.permission_notification_listener_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_notification_listener_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_notification_listener_description), special = true, ) ) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt index 383a300333a1..d5b92bc7ca43 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt @@ -25,6 +25,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.LongNonKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.nssdk.localmodel.entry.NSMbgV3 import app.aaps.core.nssdk.localmodel.entry.NSSgvV3 import app.aaps.core.nssdk.localmodel.food.NSFood @@ -215,7 +216,7 @@ class NsIncomingDataProcessor @Inject constructor( text = therapyEvent.note ?: "", validTo = dateUtil.now() + T.mins(60).msecs(), soundRes = R.raw.alarm, - actions = listOf(NotificationAction(R.string.snooze) { }) + actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.snooze)) { }) ) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index e9d30f753fee..deaf542e4263 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -23,6 +23,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.LongComposedKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.nssdk.interfaces.RunningConfiguration import app.aaps.core.nssdk.mapper.toCalibrationMbg import app.aaps.core.nssdk.mapper.toNSDeviceStatus @@ -443,7 +444,7 @@ class NSClientV3Service : DaggerService() { 30 -> app.aaps.core.ui.R.string.snooze_30m else -> app.aaps.core.ui.R.string.snooze_60m } - NotificationAction(labelRes) { + NotificationAction(TextRef.AndroidRes(labelRes)) { val snoozeMs = minutes * 60 * 1000L nsClientV3Plugin.handleClearAlarm(nsAlarm, snoozeMs) // Cascade the snooze across all alarm levels. NS itself cascades a level-2 ack down to diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt index b87f1f2bda66..a0d1e0435a76 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt @@ -26,6 +26,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sync.Sync import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.icons.IcPluginOpenHumans import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.plugins.sync.R @@ -628,7 +629,7 @@ class OpenHumansUploaderPlugin @Inject internal constructor( text = rh.gs(R.string.you_have_been_signed_out_of_open_humans) + "\n" + rh.gs(R.string.click_here_to_sign_in_again_if_this_wasnt_on_purpose), actions = listOf( - NotificationAction(CoreUiR.string.login) { + NotificationAction(TextRef.AndroidRes(CoreUiR.string.login)) { val intent = Intent(context, OHLoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index 4423535a3163..6f123dee2452 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -50,6 +50,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.IntKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withCompose import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.generateCOBString @@ -185,8 +186,8 @@ class SmsCommunicatorPlugin @Inject constructor( override fun requiredPermissions(): List = listOf( PermissionGroup( permissions = listOf(Manifest.permission.RECEIVE_SMS, Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_MMS), - rationaleTitle = R.string.permission_sms_title, - rationaleDescription = R.string.permission_sms_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_sms_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_sms_description), ) ) diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index 22c3a63ade43..e33881364343 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -123,8 +123,8 @@ class EopatchPumpPlugin @Inject constructor( override fun requiredPermissions(): List = super.requiredPermissions() + listOf( PermissionGroup( permissions = listOf(Manifest.permission.SCHEDULE_EXACT_ALARM), - rationaleTitle = R.string.permission_exact_alarm_title, - rationaleDescription = R.string.permission_exact_alarm_description, + rationaleTitle = TextRef.AndroidRes(R.string.permission_exact_alarm_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_exact_alarm_description), special = true, ) ) diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt index d95e1c40bab8..30d83349230b 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt @@ -15,6 +15,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.eopatch.EoPatchRxBus import app.aaps.pump.eopatch.alarm.AlarmCode.A005 import app.aaps.pump.eopatch.alarm.AlarmCode.A016 @@ -155,11 +156,13 @@ class AlarmManager @Inject constructor() : IAlarmManager { soundRes = if (!isCritical) app.aaps.core.ui.R.raw.error else null, actions = listOf( NotificationAction( - when (alarmCode) { - B001 -> app.aaps.core.ui.R.string.pump_resume - AlarmCode.A007 -> app.aaps.core.ui.R.string.retry - else -> app.aaps.core.ui.R.string.confirm - } + TextRef.AndroidRes( + when (alarmCode) { + B001 -> app.aaps.core.ui.R.string.pump_resume + AlarmCode.A007 -> app.aaps.core.ui.R.string.retry + else -> app.aaps.core.ui.R.string.confirm + } + ) ) { compositeDisposable.add( Single.just(isValid(alarmCode)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/notificationsSheet/NotificationBottomSheet.kt b/ui/src/main/kotlin/app/aaps/ui/compose/notificationsSheet/NotificationBottomSheet.kt index e67902fd955b..b71115765ab9 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/notificationsSheet/NotificationBottomSheet.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/notificationsSheet/NotificationBottomSheet.kt @@ -28,7 +28,9 @@ import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.notifications.AapsNotification import app.aaps.core.interfaces.notifications.NotificationCategory import app.aaps.core.interfaces.notifications.NotificationLevel +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.R +import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalDateUtil import app.aaps.core.ui.compose.icons.IcCgmInsert @@ -115,26 +117,26 @@ private fun NotificationItem( ) { if (notification.actions.isNotEmpty()) { notification.actions.forEach { action -> - ActionButton(textRes = action.buttonTextRes) { + ActionButton(text = action.buttonText) { action.action() onActionClick() onDismiss() } } } else { - ActionButton(textRes = R.string.dismiss, onClick = onDismiss) + ActionButton(text = TextRef.AndroidRes(R.string.dismiss), onClick = onDismiss) } } } } @Composable -private fun RowScope.ActionButton(textRes: Int, onClick: () -> Unit) { +private fun RowScope.ActionButton(text: TextRef, onClick: () -> Unit) { FilledTonalButton( onClick = onClick, modifier = Modifier.weight(1f) ) { - Text(text = stringResource(textRes)) + Text(text = stringResource(text)) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheet.kt b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheet.kt index bf9af9f81a1c..af6ffbdd23ed 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheet.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheet.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.plugin.PermissionGroup +import app.aaps.core.ui.compose.stringResource import app.aaps.ui.R @OptIn(ExperimentalMaterial3Api::class) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetPreviews.kt b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetPreviews.kt index 3da2317c37ad..d30ea2c9d1f8 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetPreviews.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetPreviews.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.interfaces.plugin.PermissionGroup +import app.aaps.core.keys.interfaces.TextRef import app.aaps.ui.R @Preview(showBackground = true) @@ -15,24 +16,24 @@ internal fun PermissionsSheetContentPreview() { PermissionItem( group = PermissionGroup( permissions = listOf("android.permission.BLUETOOTH_CONNECT"), - rationaleTitle = R.string.permission_sheet_title, - rationaleDescription = R.string.permission_sheet_subtitle, + rationaleTitle = TextRef.AndroidRes(R.string.permission_sheet_title), + rationaleDescription = TextRef.AndroidRes(R.string.permission_sheet_subtitle), ), granted = true ), PermissionItem( group = PermissionGroup( permissions = listOf("android.permission.POST_NOTIFICATIONS"), - rationaleTitle = R.string.permission_grant, - rationaleDescription = R.string.permission_sheet_subtitle, + rationaleTitle = TextRef.AndroidRes(R.string.permission_grant), + rationaleDescription = TextRef.AndroidRes(R.string.permission_sheet_subtitle), ), granted = false ), PermissionItem( group = PermissionGroup( permissions = listOf("android.permission.ACCESS_FINE_LOCATION"), - rationaleTitle = R.string.permission_change, - rationaleDescription = R.string.permission_sheet_subtitle, + rationaleTitle = TextRef.AndroidRes(R.string.permission_change), + rationaleDescription = TextRef.AndroidRes(R.string.permission_sheet_subtitle), alwaysShowAction = true, ), granted = true diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetContentTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetContentTest.kt index 3e7878e66658..ffd9166f3da6 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetContentTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsSheetContentTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import app.aaps.core.interfaces.plugin.PermissionGroup +import app.aaps.core.keys.interfaces.TextRef import app.aaps.ui.R import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -43,8 +44,8 @@ class PermissionsSheetContentTest { fun rendersTitleAndFiresGrant() { val group = PermissionGroup( permissions = listOf("android.permission.POST_NOTIFICATIONS"), - rationaleTitle = R.string.permission_change, - rationaleDescription = R.string.permission_sheet_subtitle + rationaleTitle = TextRef.AndroidRes(R.string.permission_change), + rationaleDescription = TextRef.AndroidRes(R.string.permission_sheet_subtitle) ) var requested: PermissionGroup? = null compose.setContent { From d143b22f3262c7eed026b3306a8c6017381bfff4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 21:47:13 +0200 Subject: [PATCH 054/146] TimeDiff instead of TimeUnit map --- .../aaps/core/interfaces/utils/DateUtil.kt | 3 +-- .../aaps/core/interfaces/utils/TimeDiff.kt | 23 +++++++++++++++++++ .../aaps/shared/impl/utils/DateUtilImpl.kt | 22 +++++++++--------- .../shared/impl/utils/DateUtilImplTest.kt | 21 +++++++++++------ .../overview/statusLights/StatusViewModel.kt | 6 ++--- 5 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt index 6613a5d30f3c..ae665901fd1a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt @@ -1,7 +1,6 @@ package app.aaps.core.interfaces.utils import app.aaps.core.interfaces.resources.ResourceHelper -import java.util.concurrent.TimeUnit /** * The Class DateUtil. A modern utility class for handling dates, times, and durations using the `java.time` API. @@ -430,7 +429,7 @@ interface DateUtil { * @return A map containing the total number of full days, leftover hours, and leftover minutes. */ //Map:{DAYS=1, HOURS=3, MINUTES=46, SECONDS=40, MILLISECONDS=0, MICROSECONDS=0, NANOSECONDS=0} - fun computeDiff(date1: Long, date2: Long): Map + fun computeDiff(date1: Long, date2: Long): TimeDiff /** * Converts a duration in milliseconds into a human-readable "age" string (e.g., "5 days 3 hours"). diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt new file mode 100644 index 000000000000..9e2341f6f909 --- /dev/null +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt @@ -0,0 +1,23 @@ +package app.aaps.core.interfaces.utils + +/** + * A duration split into whole calendar-style components. + * + * Replaces the `Map` this used to be returned as. Two reasons, + * beyond `TimeUnit` being a JVM type that cannot go to commonMain: + * + * - every lookup was `diff[TimeUnit.DAYS] ?: 0`, which reads as if the key might be missing. It never + * was - the map was always built with all seven entries - so the elvis was dead code that hid a + * guarantee rather than expressing it. + * - only [days] and [hours] are actually read anywhere. The rest stay because they cost nothing and + * dropping them would be a separate decision. + */ +data class TimeDiff( + val days: Long, + val hours: Long, + val minutes: Long, + val seconds: Long, + val milliseconds: Long, + val microseconds: Long, + val nanoseconds: Long +) diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt index a3bee3b8787e..4b49427fc603 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt @@ -8,6 +8,7 @@ import app.aaps.core.interfaces.R import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.SafeParse +import app.aaps.core.interfaces.utils.TimeDiff import java.security.SecureRandom import java.time.Clock import java.time.Instant @@ -20,7 +21,6 @@ import java.time.format.FormatStyle import java.time.temporal.ChronoField import java.time.temporal.ChronoUnit import java.util.Locale -import java.util.concurrent.TimeUnit import java.util.regex.Pattern import javax.inject.Inject import javax.inject.Singleton @@ -348,18 +348,18 @@ class DateUtilImpl @Inject constructor( } //Map:{DAYS=1, HOURS=3, MINUTES=46, SECONDS=40, MILLISECONDS=0, MICROSECONDS=0, NANOSECONDS=0} - override fun computeDiff(date1: Long, date2: Long): Map { + override fun computeDiff(date1: Long, date2: Long): TimeDiff { val duration = (date2 - date1).milliseconds return duration.toComponents { days, hours, minutes, seconds, nanoseconds -> - mapOf( - TimeUnit.DAYS to days, - TimeUnit.HOURS to hours.toLong(), - TimeUnit.MINUTES to minutes.toLong(), - TimeUnit.SECONDS to seconds.toLong(), - // Convert remaining nanoseconds into millis, micros, and nanos for the map. - TimeUnit.MILLISECONDS to nanoseconds.toLong() / 1_000_000, - TimeUnit.MICROSECONDS to (nanoseconds.toLong() / 1_000) % 1000, - TimeUnit.NANOSECONDS to nanoseconds.toLong() % 1000 + TimeDiff( + days = days, + hours = hours.toLong(), + minutes = minutes.toLong(), + seconds = seconds.toLong(), + // Remaining nanoseconds, split into millis, micros and nanos. + milliseconds = nanoseconds.toLong() / 1_000_000, + microseconds = (nanoseconds.toLong() / 1_000) % 1000, + nanoseconds = nanoseconds.toLong() % 1000 ) } } diff --git a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt index dc497ab222b8..7c9ce5cdf24f 100644 --- a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt +++ b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt @@ -1003,13 +1003,20 @@ class DateUtilImplTest { val oldResult = dateUtilOldImpl.computeDiff(date1, date2) // ASSERT - assertThat(newResult[TimeUnit.DAYS]).isEqualTo(1L) - assertThat(newResult[TimeUnit.HOURS]).isEqualTo(3L) - assertThat(newResult[TimeUnit.MINUTES]).isEqualTo(46L) - assertThat(newResult[TimeUnit.SECONDS]).isEqualTo(40L) - - // Check that the new, clearer implementation matches the old, complex one - assertThat(newResult).isEqualTo(oldResult) + assertThat(newResult.days).isEqualTo(1L) + assertThat(newResult.hours).isEqualTo(3L) + assertThat(newResult.minutes).isEqualTo(46L) + assertThat(newResult.seconds).isEqualTo(40L) + + // Still compared against the old map-returning implementation, component by component, so + // the TimeDiff conversion cannot quietly change one of them. + assertThat(newResult.days).isEqualTo(oldResult[TimeUnit.DAYS]) + assertThat(newResult.hours).isEqualTo(oldResult[TimeUnit.HOURS]) + assertThat(newResult.minutes).isEqualTo(oldResult[TimeUnit.MINUTES]) + assertThat(newResult.seconds).isEqualTo(oldResult[TimeUnit.SECONDS]) + assertThat(newResult.milliseconds).isEqualTo(oldResult[TimeUnit.MILLISECONDS]) + assertThat(newResult.microseconds).isEqualTo(oldResult[TimeUnit.MICROSECONDS]) + assertThat(newResult.nanoseconds).isEqualTo(oldResult[TimeUnit.NANOSECONDS]) } @Test diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt index 745ece6aebf3..326faf3421f2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt @@ -18,6 +18,7 @@ import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.stats.TddCalculator import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter +import app.aaps.core.interfaces.utils.TimeDiff import app.aaps.core.keys.IntKey import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.Preferences @@ -39,7 +40,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel @@ -255,8 +255,8 @@ class StatusViewModel @Inject constructor( private fun formatAge(timestamp: Long): String { val diff = dateUtil.computeDiff(timestamp, System.currentTimeMillis()) - val days = diff[TimeUnit.DAYS] ?: 0 - val hours = diff[TimeUnit.HOURS] ?: 0 + val days = diff.days + val hours = diff.hours return if (rh.shortTextMode()) { "${days}${rh.gs(app.aaps.core.interfaces.R.string.shortday)}${hours}${rh.gs(app.aaps.core.interfaces.R.string.shorthour)}" } else { From 53c7942d2bf21a566f9e309a6a5bddb2cca580dd Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 11 Aug 2026 22:36:49 +0200 Subject: [PATCH 055/146] BolusProgressState carries TextRef --- .../core/interfaces/pump/BolusProgressData.kt | 34 ++++++++++++------- .../interfaces/pump/BolusProgressDataTest.kt | 2 +- .../ui/compose/pump/PumpActivityDialog.kt | 5 +-- .../pump/PumpActivityDialogPreviews.kt | 21 ++++++------ .../compose/pump/PumpActivityFabPreviews.kt | 5 +-- .../queue/CommandQueueImplementationTest.kt | 2 +- .../clientcontrol/ClientControlReceiver.kt | 4 ++- .../clientcontrol/ClientControlRoundTrip.kt | 3 +- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 2 +- .../app/aaps/plugins/sync/wear/WearPlugin.kt | 2 +- .../ClientControlReceiverTest.kt | 16 +++++++-- .../ClientControlRoundTripTest.kt | 3 +- .../ClientControlUplinkIntegrationTest.kt | 2 +- .../plugins/sync/tizen/TizenPluginTest.kt | 16 +++++++-- .../aaps/plugins/sync/wear/WearPluginTest.kt | 2 +- .../nightscout/pump/combov2/ComboV2Plugin.kt | 9 +++-- .../app/aaps/pump/danar/comm/MsgBolusStop.kt | 3 +- .../app/aaps/pump/danar/comm/MsgError.kt | 3 +- .../danar/services/DanaRExecutionService.kt | 9 ++--- .../services/DanaRKoreanExecutionService.kt | 3 +- .../services/DanaRv2ExecutionService.kt | 5 +-- .../app/aaps/pump/danar/DanaRPluginTest.kt | 2 +- .../app/aaps/pump/danar/comm/DanaRTestBase.kt | 2 +- .../AbstractDanaRExecutionServiceTest.kt | 4 +-- .../pump/danarkorean/DanaRKoreanPluginTest.kt | 2 +- .../aaps/pump/danarv2/DanaRv2PluginTest.kt | 2 +- .../danars/emulator/BLECommIntegrationTest.kt | 2 +- .../emulator/DanaRSServiceIntegrationTest.kt | 2 +- .../comm/DanaRSPacketBolusSetStepBolusStop.kt | 3 +- .../pump/danars/services/DanaRSService.kt | 5 +-- .../app/aaps/pump/danars/DanaRSTestBase.kt | 2 +- .../pump/danars/services/DanaRSServiceTest.kt | 2 +- .../pump/diaconn/service/DiaconnG8Service.kt | 3 +- .../aaps/pump/diaconn/DiaconnG8PluginTest.kt | 2 +- .../InjectionSnackResultReportPacketTest.kt | 2 +- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 4 ++- .../pump/medtrum/services/MedtrumService.kt | 3 +- .../app/aaps/pump/medtrum/MedtrumTestBase.kt | 2 +- .../omnipod/dash/OmnipodDashPumpPlugin.kt | 3 +- 39 files changed, 125 insertions(+), 73 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt index 24c142070bc7..21f612b96e6e 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt @@ -1,8 +1,10 @@ package app.aaps.core.interfaces.pump +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.insulin.ConcentrationHelper -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -23,7 +25,6 @@ import javax.inject.Singleton @Singleton class BolusProgressData @Inject constructor( val ch: ConcentrationHelper, - val rh: ResourceHelper, @ApplicationScope private val appScope: CoroutineScope, ) { @@ -51,8 +52,8 @@ class BolusProgressData @Inject constructor( isSMB = isSMB, isPriming = isPriming, percent = 0, - status = "", - wearStatus = "", + status = TextRef.Literal(""), + wearStatus = TextRef.Literal(""), delivered = PumpInsulin(0.0), stopPressed = false, stopDeliveryEnabled = true @@ -64,7 +65,7 @@ class BolusProgressData @Inject constructor( * Called by pump drivers to report delivery progress. * Purely informational — does not control dialog lifecycle. */ - fun updateProgress(percent: Int, status: String, delivered: PumpInsulin = PumpInsulin(0.0)) { + fun updateProgress(percent: Int, status: TextRef, delivered: PumpInsulin = PumpInsulin(0.0)) { // A fresh frame proves liveness → clear any prior stall flag (lets the client dialog recover). _state.update { it?.copy(percent = percent, status = status, wearStatus = status, delivered = delivered, stalled = false) } } @@ -78,10 +79,11 @@ class BolusProgressData @Inject constructor( val insulin = state.insulin val delivered = if (state.isPriming) PumpInsulin(insulin * percent / 100) else PumpInsulin(insulin / ch.concentration * percent / 100) - val status = if (percent < 100) ch.bolusProgressString(delivered, state.isPriming) - else rh.gs(app.aaps.core.interfaces.R.string.bolus_delivered_successfully, insulin) - val wearStatus = if (percent < 100) ch.bolusProgressString(delivered, insulin, state.isPriming) - else rh.gs(app.aaps.core.interfaces.R.string.bolus_delivered_successfully, insulin) + val done = InterfacesStrings.bolus_delivered_successfully.withArgs(insulin) + val status = if (percent < 100) TextRef.Literal(ch.bolusProgressString(delivered, state.isPriming)) + else done + val wearStatus = if (percent < 100) TextRef.Literal(ch.bolusProgressString(delivered, insulin, state.isPriming)) + else done _state.update { it?.copy(percent = percent, status = status, wearStatus = wearStatus, delivered = delivered, stalled = false) } } } @@ -94,8 +96,8 @@ class BolusProgressData @Inject constructor( _state.value?.let { state -> val insulin = state.insulin val percent = (ch.fromPump(delivered, state.isPriming) / insulin * 100).toInt().coerceAtMost(100) - val status = ch.bolusProgressString(delivered, state.isPriming) - val wearStatus = ch.bolusProgressString(delivered, insulin, state.isPriming) + val status = TextRef.Literal(ch.bolusProgressString(delivered, state.isPriming)) + val wearStatus = TextRef.Literal(ch.bolusProgressString(delivered, insulin, state.isPriming)) _state.update { it?.copy(percent = percent, status = status, wearStatus = wearStatus, delivered = delivered, stalled = false) } } } @@ -209,8 +211,14 @@ data class BolusProgressState( val isSMB: Boolean, val isPriming: Boolean, val percent: Int, - val status: String, - val wearStatus: String, + /** + * Kept as an unresolved reference rather than rendered text: the dialog resolves it with Compose, + * while the wear and Tizen bridges resolve it with their own ResourceHelper before putting it on + * the wire. Those bridges are why this cannot simply be a TextRef all the way to the watch - a + * different APK cannot resolve this one's resource ids, so the phone has to render before sending. + */ + val status: TextRef, + val wearStatus: TextRef, val delivered: PumpInsulin, val stopPressed: Boolean, val stopDeliveryEnabled: Boolean, diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt index 3dfd1eec9781..2e1c06055f70 100644 --- a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt +++ b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt @@ -25,7 +25,7 @@ class BolusProgressDataTest { whenever(ch.fromPump(any(), any())).thenAnswer { (it.arguments[0] as PumpInsulin).cU } whenever(ch.bolusProgressString(any(), any())).thenReturn("") whenever(ch.bolusProgressString(any(), any(), any())).thenReturn("") - sut = BolusProgressData(ch, rh, TestScope()) + sut = BolusProgressData(ch, TestScope()) } @Test diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt index 8fae5723153a..e82dacc248c9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt @@ -175,9 +175,10 @@ private fun BolusProgressSection( // Status text — hidden when stalled: a present-tense "Delivering …" line would contradict the // "connection lost / status unknown" message and read as if delivery were still being tracked. - if (state.status.isNotEmpty() && !state.stalled) { + val statusText = stringResource(state.status) + if (statusText.isNotEmpty() && !state.stalled) { Text( - text = state.status, + text = statusText, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt index ed0d3ebb7fab..d9a960ac8db1 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.pump.PumpInsulin +import app.aaps.core.keys.interfaces.TextRef @Preview(showBackground = true, widthDp = 360) @Composable @@ -16,8 +17,8 @@ internal fun PreviewBolusInProgress() { isSMB = false, isPriming = false, percent = 45, - status = "Delivering 1.80U", - wearStatus = "Delivering 1.80U", + status = TextRef.Literal("Delivering 1.80U"), + wearStatus = TextRef.Literal("Delivering 1.80U"), delivered = PumpInsulin(1.8), stopPressed = false, stopDeliveryEnabled = true @@ -40,8 +41,8 @@ internal fun PreviewBolusStopPressed() { isSMB = false, isPriming = false, percent = 45, - status = "Delivering 1.80U", - wearStatus = "Delivering 1.80U", + status = TextRef.Literal("Delivering 1.80U"), + wearStatus = TextRef.Literal("Delivering 1.80U"), delivered = PumpInsulin(1.8), stopPressed = true, stopDeliveryEnabled = true @@ -64,8 +65,8 @@ internal fun PreviewBolusCompleted() { isSMB = false, isPriming = false, percent = 100, - status = "Bolus 4.00U delivered successfully", - wearStatus = "Bolus 4.00U delivered successfully", + status = TextRef.Literal("Bolus 4.00U delivered successfully"), + wearStatus = TextRef.Literal("Bolus 4.00U delivered successfully"), delivered = PumpInsulin(4.0), stopPressed = false, stopDeliveryEnabled = true @@ -88,8 +89,8 @@ internal fun PreviewBolusIndeterminate() { isSMB = false, isPriming = false, percent = 0, - status = "", - wearStatus = "", + status = TextRef.Literal(""), + wearStatus = TextRef.Literal(""), delivered = PumpInsulin(0.0), stopPressed = false, stopDeliveryEnabled = false @@ -112,8 +113,8 @@ internal fun PreviewBolusStalled() { isSMB = false, isPriming = false, percent = 85, - status = "Delivering 1.36U", - wearStatus = "Delivering 1.36U", + status = TextRef.Literal("Delivering 1.36U"), + wearStatus = TextRef.Literal("Delivering 1.36U"), delivered = PumpInsulin(1.36), stopPressed = false, stopDeliveryEnabled = true, diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt index 3a5c172ee460..f3574de41fae 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.pump.PumpInsulin +import app.aaps.core.keys.interfaces.TextRef @Preview(showBackground = true) @Composable @@ -29,8 +30,8 @@ internal fun PreviewPumpFabSmbPercent() { isSMB = true, isPriming = false, percent = 42, - status = "Delivering 0.13U", - wearStatus = "Delivering 0.13U", + status = TextRef.Literal("Delivering 0.13U"), + wearStatus = TextRef.Literal("Delivering 0.13U"), delivered = PumpInsulin(0.13), stopPressed = false, stopDeliveryEnabled = true diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index 791bf40c67aa..55ca409a036f 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -67,7 +67,7 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { private val commandExecutorProvider: Provider by lazy { Provider { commandExecutor } } private val testScope = CoroutineScope(Dispatchers.Unconfined) - private val bolusProgressData by lazy { BolusProgressData(ch, rh, testScope) } + private val bolusProgressData by lazy { BolusProgressData(ch, testScope) } private val profileSwitchSilentGate = ProfileSwitchSilentGate() class CommandQueueMocked( diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt index 43082f316875..47a5851c56e2 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiver.kt @@ -15,6 +15,7 @@ import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.nsclient.NSClientRepository import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.pump.BolusProgressState +import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.scenes.SceneAutomationApi @@ -113,6 +114,7 @@ class ClientControlReceiver @Inject constructor( config: Config, private val bolusProgressData: BolusProgressData, private val commandQueue: CommandQueue, + private val rh: ResourceHelper, private val aapsLogger: AAPSLogger, @ApplicationScope private val appScope: CoroutineScope ) { @@ -830,7 +832,7 @@ class ClientControlReceiver @Inject constructor( val env = ClientControlCrypto.signProgress( secret, ProgressEnvelope( - clientId = clientId, phase = phase, insulin = st.insulin, percent = st.percent, status = st.status, + clientId = clientId, phase = phase, insulin = st.insulin, percent = st.percent, status = rh.gs(st.status), delivered = st.delivered.cU, stopDeliveryEnabled = st.stopDeliveryEnabled, timestamp = dateUtil.now(), signature = "" ) ) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt index 276e60dad413..e14da6fffca8 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt @@ -19,6 +19,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.scenes.ClientControlSendResult import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.nssdk.localmodel.clientcontrol.AckEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.AckPhase import app.aaps.core.nssdk.localmodel.clientcontrol.AckStatus @@ -210,7 +211,7 @@ class ClientControlRoundTrip @Inject constructor( when (env.phase) { ProgressPhase.Active -> { if (bolusProgressData.state.value == null) bolusProgressData.start(env.insulin, isSMB = false) - bolusProgressData.updateProgress(env.percent, env.status, PumpInsulin(env.delivered)) + bolusProgressData.updateProgress(env.percent, TextRef.Literal(env.status), PumpInsulin(env.delivered)) bolusProgressData.enableStopDelivery(env.stopDeliveryEnabled) armProgressWatchdog() } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 86e4337f037e..3ea317b3c356 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -120,7 +120,7 @@ class TizenPlugin @Inject constructor( bolusProgressData.state.value?.let { state -> if (!state.isSMB) { bundle.putInt("progressPercent", state.percent) - bundle.putString("progressStatus", state.status) + bundle.putString("progressStatus", rh.gs(state.status)) } } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt index 9fc5f22390bd..d7ee6b9bca0e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt @@ -123,7 +123,7 @@ class WearPlugin @Inject constructor( if (isEnabled()) { if (state != null) { if (!state.isSMB || preferences.get(BooleanKey.WearNotifyOnSmb)) { - rxBus.send(EventMobileToWear(EventData.BolusProgress(percent = state.percent, status = state.wearStatus))) + rxBus.send(EventMobileToWear(EventData.BolusProgress(percent = state.percent, status = rh.gs(state.wearStatus)))) lastSentPercent = state.percent } } else if (lastSentPercent < 100) { diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt index ab841e4dd28b..faf5bce96d94 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlReceiverTest.kt @@ -17,6 +17,7 @@ import app.aaps.core.interfaces.protection.SecureEncrypt import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.pump.PumpInsulin +import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.scenes.SceneAutomationApi import app.aaps.core.interfaces.scenes.SceneAutomationResult @@ -28,6 +29,7 @@ import app.aaps.core.keys.StringKey import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.BooleanNonPreferenceKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.nssdk.interfaces.NSAndroidClient import app.aaps.core.nssdk.localmodel.clientcontrol.AckEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.BatchActionDto @@ -73,6 +75,7 @@ internal class ClientControlReceiverTest { @Mock private lateinit var preferences: Preferences @Mock private lateinit var aapsLogger: AAPSLogger + @Mock private lateinit var rh: ResourceHelper @Mock private lateinit var nsClientRepository: NSClientRepository @Mock private lateinit var nsClientV3Plugin: NSClientV3Plugin @Mock private lateinit var nsAndroidClient: NSAndroidClient @@ -129,6 +132,14 @@ internal class ClientControlReceiverTest { @BeforeEach fun setUp() { MockitoAnnotations.openMocks(this) + // gs(TextRef) is a DEFAULT interface method, so a mock answers null unless it is stubbed. + whenever(rh.gs(any())).thenAnswer { + when (val ref = it.getArgument(0)) { + is TextRef.Literal -> ref.text + is TextRef.Named -> ref.name + is TextRef.AndroidRes -> "S" + ref.id + } + } stored = "[]" storage.clear() storage[StringNonKey.SceneDefinitions.key] = "[]" @@ -173,6 +184,7 @@ internal class ClientControlReceiverTest { config, bolusProgressData, commandQueue, + rh, aapsLogger, appScope ) @@ -623,8 +635,8 @@ internal class ClientControlReceiverTest { } private fun progressState(percent: Int) = BolusProgressState( - insulin = 2.0, isSMB = false, isPriming = false, percent = percent, status = "x", - wearStatus = "x", delivered = PumpInsulin(1.0), stopPressed = false, stopDeliveryEnabled = true + insulin = 2.0, isSMB = false, isPriming = false, percent = percent, status = TextRef.Literal("x"), + wearStatus = TextRef.Literal("x"), delivered = PumpInsulin(1.0), stopPressed = false, stopDeliveryEnabled = true ) @Test diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt index c24d4e172517..ffd47af4267b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt @@ -25,6 +25,7 @@ import app.aaps.core.nssdk.localmodel.clientcontrol.MasterPairing import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressEnvelope import app.aaps.core.nssdk.localmodel.clientcontrol.ProgressPhase import app.aaps.core.nssdk.utils.ClientControlCrypto +import app.aaps.core.keys.interfaces.TextRef import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -147,7 +148,7 @@ internal class ClientControlRoundTripTest { sut.onProgressDoc(progressDoc(ProgressPhase.Active, percent = 40, status = "Delivering 0.8U", insulin = 2.0, delivered = 0.8)) verify(bolusProgressData).start(eq(2.0), eq(false), eq(false)) - verify(bolusProgressData).updateProgress(eq(40), eq("Delivering 0.8U"), any()) + verify(bolusProgressData).updateProgress(eq(40), eq(TextRef.Literal("Delivering 0.8U")), any()) } @Test diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt index 39b5bab00125..75f55a1b460e 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlUplinkIntegrationTest.kt @@ -212,7 +212,7 @@ class ClientControlUplinkIntegrationTest { masterScope = CoroutineScope(Dispatchers.Unconfined) masterReceiver = ClientControlReceiver( masterAuthorizedRepository, Provider { nsClientV3Plugin }, nsClientRepository, sceneAutomationApi, - offerPublisher, masterPrefs, dateUtil, uel, runningConfigurationPublisher, persistenceLayer, wizardBolusExecutor, notificationManager, masterConfig, bolusProgressData, commandQueue, aapsLogger, + offerPublisher, masterPrefs, dateUtil, uel, runningConfigurationPublisher, persistenceLayer, wizardBolusExecutor, notificationManager, masterConfig, bolusProgressData, commandQueue, rh, aapsLogger, masterScope ) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt index 566c19315c4a..27d3359553c0 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt @@ -17,6 +17,7 @@ import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.pump.PumpStatusProvider import app.aaps.core.interfaces.receivers.ReceiverStatusStore import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui +import app.aaps.core.keys.interfaces.TextRef import app.aaps.shared.tests.BundleMock import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat @@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock +import org.mockito.kotlin.any import org.mockito.kotlin.whenever internal class TizenPluginTest : TestBaseWithProfile() { @@ -39,7 +41,7 @@ internal class TizenPluginTest : TestBaseWithProfile() { @Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData @Mock lateinit var pumpStatusProvider: PumpStatusProvider - private val bolusProgressData by lazy { BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) } + private val bolusProgressData by lazy { BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } private lateinit var sut: TizenPlugin @BeforeEach @@ -48,6 +50,14 @@ internal class TizenPluginTest : TestBaseWithProfile() { aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, processedTbrEbData, profileFunction, preferences, processedDeviceStatusData, loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, pumpStatusProvider, bolusProgressData ) + // gs(TextRef) is a DEFAULT interface method, so a mock answers null unless it is stubbed. + whenever(rh.gs(any())).thenAnswer { + when (val ref = it.getArgument(0)) { + is TextRef.Literal -> ref.text + is TextRef.Named -> ref.name + is TextRef.AndroidRes -> "S" + ref.id + } + } whenever(iobCobCalculator.ads).thenReturn(autosensDataStore) whenever(autosensDataStore.lastBg()).thenReturn(InMemoryGlucoseValue(1000, 100.0, sourceSensor = SourceSensor.UNKNOWN)) runBlocking { whenever(profileFunction.getProfile()).thenReturn(effectiveProfile) } @@ -81,7 +91,7 @@ internal class TizenPluginTest : TestBaseWithProfile() { fun prepareDataTestAPS() { whenever(config.APS).thenReturn(true) bolusProgressData.start(insulin = 1.0, isSMB = false) - bolusProgressData.updateProgress(100, "Some status", PumpInsulin(1.0)) + bolusProgressData.updateProgress(100, TextRef.Literal("Some status"), PumpInsulin(1.0)) val event = EventLoopUpdateGui() val bundle = BundleMock.mocked() sut.prepareData(event, bundle) @@ -122,7 +132,7 @@ internal class TizenPluginTest : TestBaseWithProfile() { fun prepareDataTestAAPSClient() { whenever(config.APS).thenReturn(false) bolusProgressData.start(insulin = 1.0, isSMB = false) - bolusProgressData.updateProgress(100, "Some status", PumpInsulin(1.0)) + bolusProgressData.updateProgress(100, TextRef.Literal("Some status"), PumpInsulin(1.0)) val event = EventLoopUpdateGui() val bundle = BundleMock.mocked() sut.prepareData(event, bundle) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt index 4544e5480368..129de7351603 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt @@ -24,6 +24,6 @@ class WearPluginTest : TestBaseWithProfile() { @BeforeEach fun prepare() { rateLimit = RateLimit(dateUtil) - wearPlugin = WearPlugin(aapsLogger, rh, aapsSchedulers, preferences, fabricPrivacy, rxBus, context, dataHandlerMobile, dataLayerListenerServiceMobileHelper, config, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), persistenceLayer, scenes) + wearPlugin = WearPlugin(aapsLogger, rh, aapsSchedulers, preferences, fabricPrivacy, rxBus, context, dataHandlerMobile, dataLayerListenerServiceMobileHelper, config, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), persistenceLayer, scenes) } } diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index de929154a94c..e6d33e6f639f 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -9,6 +9,7 @@ import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.pump.defs.TimeChangeType +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.PluginConstraints @@ -39,6 +40,8 @@ import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs import app.aaps.core.ui.compose.icons.IcPluginCombo import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import info.nightscout.comboctl.android.AndroidBluetoothInterface @@ -945,14 +948,14 @@ class ComboV2Plugin @Inject constructor( is RTCommandProgressStage.DeliveringBolus -> { val percent = (progressReport.overallProgress * 100).toInt() val totalInsulin = bolusProgressData.state.value?.insulin ?: detailedBolusInfo.insulin - val status = if (percent == 100) rh.gs(app.aaps.core.interfaces.R.string.bolus_delivered_successfully, totalInsulin) - else rh.gs(app.aaps.core.interfaces.R.string.bolus_delivering, totalInsulin * percent / 100.0) + val status = if (percent == 100) InterfacesStrings.bolus_delivered_successfully.withArgs(totalInsulin) + else InterfacesStrings.bolus_delivering.withArgs(totalInsulin * percent / 100.0) bolusProgressData.updateProgress(percent, status) } BasicProgressStage.Finished -> { val percent = (progressReport.overallProgress * 100).toInt() - bolusProgressData.updateProgress(percent, "Bolus finished, performing post-bolus checks") + bolusProgressData.updateProgress(percent, TextRef.Literal("Bolus finished, performing post-bolus checks")) } else -> Unit diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgBolusStop.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgBolusStop.kt index 890dd39e76df..17cdbd1b29c3 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgBolusStop.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgBolusStop.kt @@ -1,6 +1,7 @@ package app.aaps.pump.danar.comm import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.keys.interfaces.TextRef import dagger.android.HasAndroidInjector class MsgBolusStop( @@ -20,7 +21,7 @@ class MsgBolusStop( bolusProgressData.updateProgress(percent = 100) } else { val currentPercent = bolusProgressData.state.value?.percent ?: 0 - bolusProgressData.updateProgress(currentPercent, rh.gs(app.aaps.pump.dana.R.string.overview_bolusprogress_stoped)) + bolusProgressData.updateProgress(currentPercent, TextRef.AndroidRes(app.aaps.pump.dana.R.string.overview_bolusprogress_stoped)) } } } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgError.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgError.kt index b4b07ee60faf..3cd43ab175b4 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgError.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgError.kt @@ -1,6 +1,7 @@ package app.aaps.pump.danar.comm import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.keys.interfaces.TextRef import dagger.android.HasAndroidInjector import kotlinx.coroutines.runBlocking @@ -26,7 +27,7 @@ class MsgError( if (errorCode < 8) { // bolus delivering stopped danaPump.bolusStopped = true val currentPercent = bolusProgressData.state.value?.percent ?: 0 - bolusProgressData.updateProgress(currentPercent, errorString) + bolusProgressData.updateProgress(currentPercent, TextRef.Literal(errorString)) // at least on Occlusion pump stops communication. Try to force reconnecting activePlugin.activePump.disconnect("Error from pump received") failed = true diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt index 326de5a0df3a..03b40f862b3a 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt @@ -16,6 +16,7 @@ import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.rx.events.EventShowSnackbar +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.dana.keys.DanaIntKey @@ -245,7 +246,7 @@ class DanaRExecutionService : AbstractDanaRExecutionService() { while (System.currentTimeMillis() < expectedEnd) { val waitTime = expectedEnd - System.currentTimeMillis() val currentPercent = bolusProgressData.state.value?.percent ?: 0 - bolusProgressData.updateProgress(currentPercent, rh.gs(R.string.waitingforestimatedbolusend, waitTime / 1000), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) + bolusProgressData.updateProgress(currentPercent, TextRef.AndroidRes(R.string.waitingforestimatedbolusend, listOf(waitTime / 1000)), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) SystemClock.sleep(1000) } connect() @@ -256,17 +257,17 @@ class DanaRExecutionService : AbstractDanaRExecutionService() { } if (!isConnected) { rxBus.send(EventShowSnackbar(rh.gs(app.aaps.core.ui.R.string.treatmentdeliveryerror), EventShowSnackbar.Type.Error)) - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: "", PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: TextRef.Literal(""), PumpInsulin(0.0)) return false } mSerialIOThread?.sendMessage(MsgStatus(injector)) val lastBolusTime = danaPump.lastBolusTime if (lastBolusTime != null && lastBolusTime > System.currentTimeMillis() - 2 * 60 * 1000L) { // last bolus max 2 min old val lastAmount = danaPump.lastBolusAmount ?: 0.0 - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: "", PumpInsulin(lastAmount)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: TextRef.Literal(""), PumpInsulin(lastAmount)) aapsLogger.debug(LTag.PUMP, "Used bolus amount from history: " + danaPump.lastBolusAmount) } else { - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: "", PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: TextRef.Literal(""), PumpInsulin(0.0)) aapsLogger.debug(LTag.PUMP, "Bolus amount in history too old: " + dateUtil.dateAndTimeStringNullable(lastBolusTime)) return false } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt index 72b700e8c0c3..c4eed5133a17 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt @@ -16,6 +16,7 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.danar.DanaRPlugin @@ -202,7 +203,7 @@ class DanaRKoreanExecutionService : AbstractDanaRExecutionService() { if (!danaPump.bolusStopped) { mSerialIOThread?.sendMessage(start) } else { - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: "", PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: TextRef.Literal(""), PumpInsulin(0.0)) return false } // Arm the 15s comm watchdog from bolus start (was never initialised → a stale timestamp diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt index e2be9f940eae..aca54f2f61a9 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt @@ -18,6 +18,7 @@ import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.ui.UiInteraction +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.R import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.dana.keys.DanaIntKey @@ -269,7 +270,7 @@ class DanaRv2ExecutionService : AbstractDanaRExecutionService() { if (!danaPump.bolusStopped) { mSerialIOThread?.sendMessage(start) } else { - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: "", PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, bolusProgressData.state.value?.status ?: TextRef.Literal(""), PumpInsulin(0.0)) return false } // Arm the 15s comm watchdog from bolus start (was never initialised → a stale timestamp @@ -294,7 +295,7 @@ class DanaRv2ExecutionService : AbstractDanaRExecutionService() { val expectedEnd = bolusStart + bolusDurationInMSec + 2000 while (System.currentTimeMillis() < expectedEnd) { val waitTime = expectedEnd - System.currentTimeMillis() - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, rh.gs(R.string.waitingforestimatedbolusend, waitTime / 1000), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, TextRef.AndroidRes(R.string.waitingforestimatedbolusend, listOf(waitTime / 1000)), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) SystemClock.sleep(1000) } // do not call loadEvents() directly, reconnection may be needed diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt index e5fd2b194bd0..046c3c74d2d9 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt @@ -38,7 +38,7 @@ class DanaRPluginTest : TestBaseWithProfile() { danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRPlugin = DanaRPlugin( aapsLogger, rh, preferences, config, commandQueue, aapsSchedulers, rxBus, context, activePlugin, danaPump, dateUtil, fabricPrivacy, pumpSync, - notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider + notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/comm/DanaRTestBase.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/comm/DanaRTestBase.kt index bbce32274f66..461fe54b8951 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/comm/DanaRTestBase.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/comm/DanaRTestBase.kt @@ -35,7 +35,7 @@ open class DanaRTestBase : TestBaseWithProfile() { @Mock lateinit var uiInteraction: UiInteraction private val testScope = CoroutineScope(Dispatchers.Unconfined) - val bolusProgressData by lazy { BolusProgressData(ch, rh, testScope) } + val bolusProgressData by lazy { BolusProgressData(ch, testScope) } @BeforeEach fun setup() { diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt index b0eda527cd5b..d3d55eaf658c 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt @@ -39,7 +39,7 @@ class AbstractDanaRExecutionServiceTest : TestBaseWithProfile() { injector.aapsLogger = aapsLogger injector.rh = rh injector.danaPump = danaPump - injector.bolusProgressData = BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) + injector.bolusProgressData = BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } } } @@ -81,7 +81,7 @@ class AbstractDanaRExecutionServiceTest : TestBaseWithProfile() { testService.pumpSync = pumpSync testService.activePlugin = activePlugin testService.notificationManager = notificationManager - testService.bolusProgressData = BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) + testService.bolusProgressData = BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) testService.pumpEnactResultProvider = pumpEnactResultProvider testService.rfcommTransport = mock() testService.injector = injector diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt index ece4fce7f319..bb481a294a12 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt @@ -38,7 +38,7 @@ class DanaRKoreanPluginTest : TestBaseWithProfile() { danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRPlugin = DanaRKoreanPlugin( aapsLogger, aapsSchedulers, rxBus, context, rh, activePlugin, commandQueue, danaPump, dateUtil, fabricPrivacy, - pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider + pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt index e6fc16f2cf71..a313a7ee4d36 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt @@ -42,7 +42,7 @@ class DanaRv2PluginTest : TestBaseWithProfile() { danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRv2Plugin = DanaRv2Plugin( aapsLogger, aapsSchedulers, rxBus, context, rh, activePlugin, commandQueue, danaPump, detailedBolusInfoStorage, - temporaryBasalStorage, dateUtil, fabricPrivacy, pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider + temporaryBasalStorage, dateUtil, fabricPrivacy, pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/BLECommIntegrationTest.kt b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/BLECommIntegrationTest.kt index e77684a38cb1..0b1d445a6409 100644 --- a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/BLECommIntegrationTest.kt +++ b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/BLECommIntegrationTest.kt @@ -396,7 +396,7 @@ class BLECommIntegrationTest : TestBase() { bleComm.sendMessage(startPacket) // Stop the bolus - val stopPacket = DanaRSPacketBolusSetStepBolusStop(aapsLogger, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), rh, danaPump) + val stopPacket = DanaRSPacketBolusSetStepBolusStop(aapsLogger, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), rh, danaPump) bleComm.sendMessage(stopPacket) assertThat(stopPacket.isReceived).isTrue() diff --git a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt index a7cd5a6adc77..c6a4156da869 100644 --- a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt +++ b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt @@ -125,7 +125,7 @@ class DanaRSServiceIntegrationTest : TestBase() { @Mock lateinit var ch: ConcentrationHelper @Mock lateinit var profile: Profile - private val bolusProgressData by lazy { BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) } + private val bolusProgressData by lazy { BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } private lateinit var danaPump: DanaPump private lateinit var bleEncryption: BleEncryption private lateinit var emulatorTransport: EmulatorBleTransport diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusSetStepBolusStop.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusSetStepBolusStop.kt index f09b759134d6..de80fb66abd7 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusSetStepBolusStop.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusSetStepBolusStop.kt @@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.DanaPump import app.aaps.pump.danars.encryption.BleEncryption import javax.inject.Inject @@ -36,7 +37,7 @@ open class DanaRSPacketBolusSetStepBolusStop @Inject constructor( bolusProgressData.updateProgress(100) } else { val currentPercent = bolusProgressData.state.value?.percent ?: 0 - bolusProgressData.updateProgress(currentPercent, rh.gs(app.aaps.pump.dana.R.string.overview_bolusprogress_stoped)) + bolusProgressData.updateProgress(currentPercent, TextRef.AndroidRes(app.aaps.pump.dana.R.string.overview_bolusprogress_stoped)) } } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt index 2234c5824093..d3fada360828 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt @@ -32,6 +32,7 @@ import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.DanaPump import app.aaps.pump.dana.R import app.aaps.pump.dana.comm.RecordTypes @@ -372,7 +373,7 @@ class DanaRSService : DaggerService() { val expectedEnd = bolusStart + bolusDurationInMSec + 2000 while (System.currentTimeMillis() < expectedEnd) { val waitTime = expectedEnd - System.currentTimeMillis() - bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, rh.gs(R.string.waitingforestimatedbolusend, waitTime / 1000), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) + bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 0, TextRef.AndroidRes(R.string.waitingforestimatedbolusend, listOf(waitTime / 1000)), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) SystemClock.sleep(1000) } // do not call loadEvents() directly, reconnection may be needed @@ -381,7 +382,7 @@ class DanaRSService : DaggerService() { // reread bolus status rxBus.send(EventPumpStatusChanged(rh.gs(R.string.gettingbolusstatus))) sendMessage(danaRSPacketBolusGetStepBolusInformation.get()) // last bolus - bolusProgressData.updateProgress(100, rh.gs(app.aaps.core.interfaces.R.string.disconnecting), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) + bolusProgressData.updateProgress(100, TextRef.AndroidRes(app.aaps.core.interfaces.R.string.disconnecting), bolusProgressData.state.value?.delivered ?: PumpInsulin(0.0)) rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTING)) } return !start.failed && !connectionBroken diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSTestBase.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSTestBase.kt index 1479275e4f7e..8451543a58fd 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSTestBase.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSTestBase.kt @@ -12,7 +12,7 @@ import org.mockito.kotlin.whenever open class DanaRSTestBase : TestBaseWithProfile() { - val bolusProgressData by lazy { BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) } + val bolusProgressData by lazy { BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } lateinit var danaPump: DanaPump @BeforeEach diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt index 3cece8a5ec65..23e6a30d8f12 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt @@ -67,7 +67,7 @@ class DanaRSServiceTest : TestBaseWithProfile() { danaRSService.fabricPrivacy = fabricPrivacy danaRSService.pumpSync = pumpSync danaRSService.dateUtil = dateUtil - danaRSService.bolusProgressData = BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) + danaRSService.bolusProgressData = BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) danaRSService.pumpEnactResultProvider = pumpEnactResultProvider danaRSService.danaRSPacketGeneralInitialScreenInformation = danaRSPacketGeneralInitialScreenInformationProvider danaRSService.danaRSPacketOptionSetUserOption = danaRSPacketOptionSetUserOptionProvider diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt index 1cb11f6a4ce1..f15679bce7f3 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt @@ -33,6 +33,7 @@ import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.diaconn.DiaconnG8Plugin import app.aaps.pump.diaconn.DiaconnG8Pump import app.aaps.pump.diaconn.R @@ -535,7 +536,7 @@ class DiaconnG8Service : DaggerService() { progressPercent = ((totalwaitTime - waitTime) * 100 / totalwaitTime).toInt() } val percent = min(progressPercent, 100) - bolusProgressData.updateProgress(percent, rh.gs(R.string.waitingforbolusend)) + bolusProgressData.updateProgress(percent, TextRef.AndroidRes(R.string.waitingforbolusend)) } SystemClock.sleep(200) } diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt index f67cb634202e..6235c0432b32 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt @@ -46,7 +46,7 @@ class DiaconnG8PluginTest : TestBaseWithProfile() { diaconnG8Plugin = DiaconnG8Plugin( aapsLogger, rh, preferences, commandQueue, rxBus, context, diaconnG8Pump, pumpSync, detailedBolusInfoStorage, temporaryBasalStorage, fabricPrivacy, dateUtil, aapsSchedulers, - diaconnHistoryDatabase, pumpEnactResultProvider, BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)), blePreCheck + diaconnHistoryDatabase, pumpEnactResultProvider, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), blePreCheck ) } diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/packet/InjectionSnackResultReportPacketTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/packet/InjectionSnackResultReportPacketTest.kt index 93366d7135b9..8243b29102e0 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/packet/InjectionSnackResultReportPacketTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/packet/InjectionSnackResultReportPacketTest.kt @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test class InjectionSnackResultReportPacketTest : TestBaseWithProfile() { private lateinit var diaconnG8Pump: DiaconnG8Pump - private val bolusProgressData by lazy { BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) } + private val bolusProgressData by lazy { BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } private val packetInjector = HasAndroidInjector { AndroidInjector { diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index e33881364343..3dc21da22c66 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -8,6 +8,7 @@ import app.aaps.core.data.pump.defs.PumpDescription import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.pump.defs.TimeChangeType import app.aaps.core.data.time.T +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.plugin.PermissionGroup @@ -35,6 +36,7 @@ import app.aaps.core.interfaces.utils.Round import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs import app.aaps.core.keys.interfaces.withEntries import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.icons.IcPluginEopatch @@ -358,7 +360,7 @@ class EopatchPumpPlugin @Inject constructor( .subscribeOn(aapsSchedulers.io) .observeOn(aapsSchedulers.main) .subscribe { - val status = rh.gs(app.aaps.core.interfaces.R.string.bolus_delivered_successfully, (it.injectedBolusAmount * 0.05f)) + val status = InterfacesStrings.bolus_delivered_successfully.withArgs(it.injectedBolusAmount * 0.05f) bolusProgressData.updateProgress(bolusProgressData.state.value?.percent ?: 100, status) } ) diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt index e133336e0cd3..97e2c21cb702 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt @@ -31,6 +31,7 @@ import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtrum.MedtrumPlugin import app.aaps.pump.medtrum.MedtrumPump import app.aaps.pump.medtrum.R @@ -399,7 +400,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { // Queue-worker deadlock guard — don't unwrap the .launch. See CommandQueue kdoc. scope.launch { commandQueue.readStatus(rh.gs(R.string.bolus_error)) } // make sure if anything is delivered (which is highly unlikely at this point) we get it medtrumPump.bolusDone = true - bolusProgressData.updateProgress(percent = 0, status = "") + bolusProgressData.updateProgress(percent = 0, status = TextRef.Literal("")) return false } diff --git a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/MedtrumTestBase.kt b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/MedtrumTestBase.kt index 7291ca8c3884..09854c811969 100644 --- a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/MedtrumTestBase.kt +++ b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/MedtrumTestBase.kt @@ -21,7 +21,7 @@ open class MedtrumTestBase : TestBaseWithProfile() { @Mock lateinit var pumpSync: PumpSync @Mock lateinit var temporaryBasalStorage: TemporaryBasalStorage - val bolusProgressData by lazy { BolusProgressData(ch, rh, CoroutineScope(Dispatchers.Unconfined)) } + val bolusProgressData by lazy { BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) } lateinit var medtrumPump: MedtrumPump @BeforeEach diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 4113bd222453..08d535ca1b52 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -37,6 +37,7 @@ import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventRefreshOverview import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.icons.IcPluginOmnipod import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.core.utils.DateTimeUtil @@ -739,7 +740,7 @@ class OmnipodDashPumpPlugin @Inject constructor( } private fun updateBolusProgressDialog(msg: String, percent: Int) { - bolusProgressData.updateProgress(percent, msg) + bolusProgressData.updateProgress(percent, TextRef.Literal(msg)) } private fun waitForBolusDeliveryToComplete( From 373bac2651efce3dc856cc4c142888d820f601f7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 07:20:30 +0200 Subject: [PATCH 056/146] BolusProgressData provided instead of annotated --- .../core/interfaces/pump/BolusProgressData.kt | 13 +++++++------ .../implementation/di/ImplementationModule.kt | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt index 21f612b96e6e..6410e48c86e1 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt @@ -1,7 +1,6 @@ package app.aaps.core.interfaces.pump import app.aaps.core.interfaces.InterfacesStrings -import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs @@ -13,19 +12,21 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicLong -import javax.inject.Inject -import javax.inject.Singleton /** * Core-controlled bolus progress state. * * Lifecycle is managed by the command queue (start/complete/clear), * pump drivers only report progress via [updateProgress]. + * + * Deliberately carries no DI annotations. `javax.inject` is a JVM library and does not resolve in + * commonMain, so a class meant to be shared cannot be annotated - the Android graph provides it + * instead (`ImplementationModule.provideBolusProgressData`, which keeps the singleton scope and + * supplies the application-scoped CoroutineScope). */ -@Singleton -class BolusProgressData @Inject constructor( +class BolusProgressData( val ch: ConcentrationHelper, - @ApplicationScope private val appScope: CoroutineScope, + private val appScope: CoroutineScope, ) { private val _state = MutableStateFlow(null) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt b/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt index 34b91edd8663..57fdb926829c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt @@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.APSResult import app.aaps.core.interfaces.aps.AutosensData import app.aaps.core.interfaces.bolus.BatchExecutor import app.aaps.core.interfaces.db.ProcessedTbrEbData +import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.insulin.InsulinManager import app.aaps.core.interfaces.iob.GlucoseStatusProvider @@ -28,6 +29,7 @@ import app.aaps.core.interfaces.protection.PasswordCheck import app.aaps.core.interfaces.protection.ProtectionCheck import app.aaps.core.interfaces.protection.SecureEncrypt import app.aaps.core.interfaces.pump.BlePreCheck +import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.pump.DetailedBolusInfoStorage import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.pump.PumpStatusProvider @@ -103,10 +105,13 @@ import app.aaps.implementation.utils.TrendCalculatorImpl import app.aaps.implementation.utils.fabric.FabricPrivacyImpl import dagger.Binds import dagger.Module +import dagger.Provides import dagger.android.ContributesAndroidInjector import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.Multibinds +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope @Module( includes = [ @@ -119,6 +124,18 @@ import dagger.multibindings.Multibinds @Suppress("unused") class ImplementationModule { + /** + * [BolusProgressData] is a plain class rather than an `@Inject constructor` one, because + * `javax.inject` cannot be used from code that is meant to reach commonMain. Providing it here + * keeps the graph identical: same singleton scope, same application-scoped CoroutineScope. + */ + @Provides + @Singleton + fun provideBolusProgressData( + ch: ConcentrationHelper, + @ApplicationScope appScope: CoroutineScope + ): BolusProgressData = BolusProgressData(ch, appScope) + @Module @InstallIn(SingletonComponent::class) interface Bindings { From c88bac5418ab62ced954c6573dc2a7a3f9d74a8c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 07:35:11 +0200 Subject: [PATCH 057/146] Mark CustomAction as intentionally kept --- _docs/KMP_IOS_FEASIBILITY.md | 22 +++++++++++++++++++ .../interfaces/pump/actions/CustomAction.kt | 14 ++++++++++++ .../pump/actions/CustomActionType.kt | 6 +++++ 3 files changed, 42 insertions(+) diff --git a/_docs/KMP_IOS_FEASIBILITY.md b/_docs/KMP_IOS_FEASIBILITY.md index 9dcef139e27d..7c6e8fd7d738 100644 --- a/_docs/KMP_IOS_FEASIBILITY.md +++ b/_docs/KMP_IOS_FEASIBILITY.md @@ -1747,6 +1747,28 @@ today, in this zone", which any roughly-right implementation passes. It now runs both 2026 transitions in Europe/Prague, with a guard test asserting the sweep really crosses an offset change - otherwise a UTC CI machine makes the whole thing vacuous, the same trap as the iOS workflow. +### Decisions taken, so they are not re-opened by the next analysis + +**`CustomAction` / `CustomActionType` stay, even though they are dead.** A seven-agent pass over the +pump layer classified them as safe to eliminate, with good evidence: nothing constructs a +`CustomAction` anywhere, the only `CustomActionType` implementation is referenced only by its own +declaration, and `git log -S` traces the loss to commit `060ec9d218` ("MDT: compose migration"), which +deleted MedtronicPumpPlugin's wake-up-and-tune / clear-bolus-block / reset-RileyLink actions. + +That evidence is right and the conclusion is still no: this is a deliberate **extension point** for +pump drivers that fell out of use as a side effect of a UI migration, not accidental cruft. Deleting +it would foreclose bringing those actions back. The retention is recorded in the KDoc of both types, +because any future dead-code sweep will find them again and the finding will look new. + +When the feature is revived, `CustomAction.name` is an Android string resource id and needs the same +`TextRef` treatment as everything else here. + +**The `QuickWizardEditor` re-indentation stays in the branch.** ~260 of its ~290 changed lines are a +re-indent: the body of `if (mode == QuickWizardMode.WIZARD) {` was previously indented at the same +level as the `if` itself, which reads as though the block is not there. The new indentation is +correct, and the file had to be touched for the `TextRef` migration anyway. It does bury the ~30 real +lines during review - that is the known cost, accepted rather than overlooked. + --- ## 9. Open decisions diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt index 2e6caee9667d..16f710e4ee94 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt @@ -2,6 +2,20 @@ package app.aaps.core.interfaces.pump.actions import androidx.compose.ui.graphics.vector.ImageVector +/** + * A pump driver's own action, shown in the Actions tab. + * + * **Currently constructed by nothing, and that is intentional - do not delete it as dead code.** + * The only producer was MedtronicPumpPlugin (wake-up-and-tune, clear-bolus-block, reset-RileyLink), + * removed by commit 060ec9d218 "MDT: compose migration". The extension point is kept so those + * actions, or a driver's equivalent, can come back without redesigning the contract. + * + * A dead-code sweep will flag this, [CustomActionType], [app.aaps.core.interfaces.pump.Pump.getCustomActions] + * and `Pump.executeCustomAction` together - they are one feature, retained deliberately. + * + * For the multiplatform work: [name] is an Android string resource id, so it needs the usual TextRef + * treatment whenever the feature is actually revived. + */ data class CustomAction( val name: Int, val customActionType: CustomActionType, diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt index 370695b0c480..100414cf4ab7 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt @@ -1,5 +1,11 @@ package app.aaps.core.interfaces.pump.actions +/** + * Identifies a [CustomAction] when it is dispatched back to its driver. + * + * Retained deliberately along with [CustomAction] - see the note there before removing it as dead + * code. Its only implementation, `MedtronicCustomActionType`, is likewise unreferenced today. + */ interface CustomActionType { fun getKey(): String From d23b9be3f1a0819805b07052a9b9b690eb125a2e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 07:53:23 +0200 Subject: [PATCH 058/146] Characterize profile JSON before conversion --- .../ProfileJsonCharacterizationTest.kt | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt new file mode 100644 index 000000000000..34438da1bcce --- /dev/null +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt @@ -0,0 +1,186 @@ +package app.aaps.core.objects.profile + +import android.content.Context +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.objects.extensions.blockFromJsonArray +import app.aaps.shared.impl.utils.DateUtilImpl +import app.aaps.shared.tests.TestBase +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock + +/** + * Characterization of the profile JSON contract, written BEFORE the `org.json` -> kotlinx conversion. + * + * The profile document is the highest risk thing left to convert: it feeds Nightscout, it is read + * back from other uploaders and from older AAPS versions, and every number in it is a dosing + * parameter. So this file pins what `org.json` does today rather than what the schema looks like - + * the schema is the easy part. + * + * Three findings drive it, all measured rather than assumed: + * + * 1. Real profile documents carry numbers as **quoted strings** (`"value":"0.1"`) as well as real + * numbers - both shapes occur in the wild. Both `org.json` AND kotlinx coerce the quoted form, + * so this is NOT the hazard it looks like - measured, not assumed. See the test below. + * 2. A malformed schedule makes `blockFromJsonArray` return **null** (an invalid profile), not throw. + * Strict parsing would turn that into an exception, i.e. a crash where today there is a rejected + * profile. + * 3. `org.json` writes a whole-numbered double without its fraction, so a basal of `1.0` is uploaded + * as `1`. kotlinx writes `1.0`. A full profile has ~120 such values. + * + * When the conversion happens these assertions must still hold. Where a divergence is intentional it + * is asserted as a divergence here, not quietly skipped. + */ +class ProfileJsonCharacterizationTest : TestBase() { + + @Mock lateinit var rh: ResourceHelper + @Mock lateinit var context: Context + + private lateinit var dateUtil: DateUtil + + @BeforeEach fun setup() { + dateUtil = DateUtilImpl(context) + } + + private fun schedule(vararg entries: Pair): JSONArray = + JSONArray().also { arr -> + entries.forEach { (time, value) -> + arr.put(JSONObject().put("time", time).put("value", value)) + } + } + + // ---------------------------------------------------------------- 1. numbers arrive as strings + + /** + * `"0.1"` is a JSON *string*, and the whole profile spine depends on it being read as a number. + */ + @Test fun `schedule values are read from quoted strings`() { + val blocks = blockFromJsonArray(schedule("00:00" to "0.1", "12:00" to "0.25"), dateUtil) + + assertThat(blocks).isNotNull() + assertThat(blocks!!).hasSize(2) + assertThat(blocks[0].amount).isEqualTo(0.1) + assertThat(blocks[1].amount).isEqualTo(0.25) + } + + /** ...and as real JSON numbers, which is what AAPS itself writes. Both shapes must work. */ + @Test fun `schedule values are read from real numbers too`() { + val arr = JSONArray() + .put(JSONObject().put("time", "00:00").put("value", 0.1)) + .put(JSONObject().put("time", "12:00").put("value", 0.25)) + val blocks = blockFromJsonArray(arr, dateUtil) + + assertThat(blocks).isNotNull() + assertThat(blocks!![0].amount).isEqualTo(0.1) + assertThat(blocks[1].amount).isEqualTo(0.25) + } + + /** An integer-looking string is a double too - `"6"` is an ISF, not the integer 6. */ + @Test fun `integer looking strings become doubles`() { + val blocks = blockFromJsonArray(schedule("00:00" to "6"), dateUtil) + assertThat(blocks!![0].amount).isEqualTo(6.0) + } + + // ---------------------------------------------------------------- 2. failure is null, not throw + + @Test fun `a non numeric value yields an invalid profile rather than an exception`() { + assertThat(blockFromJsonArray(schedule("00:00" to "not a number"), dateUtil)).isNull() + } + + @Test fun `a missing value key yields an invalid profile rather than an exception`() { + val arr = JSONArray().put(JSONObject().put("time", "00:00")) + assertThat(blockFromJsonArray(arr, dateUtil)).isNull() + } + + /** + * Hour alignment is a real rule, not an accident: a schedule that does not start on the hour is + * rejected. Worth pinning because a rewritten parser could easily drop the check. + */ + @Test fun `a schedule not aligned to whole hours is rejected`() { + assertThat(blockFromJsonArray(schedule("00:30" to "0.1", "12:00" to "0.2"), dateUtil)).isNull() + } + + @Test fun `a null array yields null`() { + assertThat(blockFromJsonArray(null, dateUtil)).isNull() + } + + // ---------------------------------------------------------------- 3. the write side diverges + + /** + * The divergence that would change bytes uploaded to Nightscout. + * + * A profile has 24 basal + 24 ISF + 24 IC + 48 target entries, so a document can differ in ~120 + * places without a single value being wrong. Both parse back identically - what breaks is any + * consumer comparing documents as text. + */ + @Test fun `whole numbered profile values serialize differently`() { + val org = JSONObject().put("value", 1.0).toString() + val kotlinx = buildJsonObject { put("value", JsonPrimitive(1.0)) }.toString() + + assertThat(org).isEqualTo("""{"value":1}""") + assertThat(kotlinx).isEqualTo("""{"value":1.0}""") + assertWithMessage("if this ever passes, the divergence is gone and the shim can drop it") + .that(org).isNotEqualTo(kotlinx) + } + + /** A fractional basal rate is written the same by both, so only the whole-numbered case matters. */ + @Test fun `fractional profile values serialize identically`() { + assertThat(JSONObject().put("value", 0.825).toString()) + .isEqualTo(buildJsonObject { put("value", JsonPrimitive(0.825)) }.toString()) + } + + // ---------------------------------------------------------------- 4. what strict parsing costs (less than expected) + + /** + * Quoted numbers are NOT the migration hazard they look like. + * + * The expectation going in was that a `Double` field would reject `"0.1"` without `isLenient`, + * because it is a JSON string where a number belongs. Measured, that is false: kotlinx coerces it + * in default (strict) configuration. Pinned here so the conversion does not switch `isLenient` on + * for a problem that does not exist - leniency would also start accepting genuinely malformed + * input that is rejected today. + * + * Caveat, stated rather than glossed: this exercises the built-in `Double` serializer directly and + * through a map. The `@Serializable` data-class code path could not be exercised from this module + * (no serialization plugin here), so if the conversion uses generated serializers, re-measure. + */ + @Test fun `kotlinx accepts quoted numbers without leniency`() { + assertThat(Json.decodeFromString(Double.serializer(), "\"0.1\"")).isEqualTo(0.1) + assertThat( + Json.decodeFromString(MapSerializer(String.serializer(), Double.serializer()), """{"value":"0.1"}""") + ).containsEntry("value", 0.1) + + // ...matching what org.json has always done, which is why the profile spine works today. + assertThat(JSONObject("""{"value":"0.1"}""").getDouble("value")).isEqualTo(0.1) + } + + /** + * The divergence that only real data revealed: `org.json` escapes a forward slash, kotlinx does + * not. Every profile carries two slashed strings - `units` (`mg/dl`) and `timezone` + * (`Africa/Cairo`) - so this changes bytes on every single upload, independently of the + * whole-number issue above. + * + * Both decode to the same string, so nothing inside AAPS notices. A document hash or a byte + * comparison on the Nightscout side would. + */ + @Test fun `forward slashes are escaped by org json but not by kotlinx`() { + val org = JSONObject().put("units", "mg/dl").toString() + val kotlinx = buildJsonObject { put("units", JsonPrimitive("mg/dl")) }.toString() + + assertThat(org).isEqualTo("""{"units":"mg\/dl"}""") + assertThat(kotlinx).isEqualTo("""{"units":"mg/dl"}""") + // ...and both read back the same, which is why this hides so well. + assertThat(JSONObject(org).getString("units")) + .isEqualTo(Json.parseToJsonElement(kotlinx).let { (it as kotlinx.serialization.json.JsonObject) }["units"]!!.let { (it as JsonPrimitive).content }) + } +} From 74644c76675c46f7341f49cd5c30d2b05ab8e78c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 08:14:55 +0200 Subject: [PATCH 059/146] Add real Nightscout shapes to profile characterization --- .../data/datetime/IsoDateParserParityTest.kt | 4 ++ .../ProfileJsonCharacterizationTest.kt | 66 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt b/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt index b4550f7cace9..dd068e26026c 100644 --- a/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt +++ b/core/data/src/jvmTest/kotlin/app/aaps/core/data/datetime/IsoDateParserParityTest.kt @@ -48,6 +48,10 @@ class IsoDateParserParityTest { "2017-11-19T22:50:34.417+0200", // from the historical DateUtil test "2017-12-03T16:09:25.000Z", "2017-12-22T00:32:30Z", + // Real Nightscout data: `startDate` carries SEVEN fractional digits while `created_at` in the + // same document carries three. A parser pinned to `.SSS` would reject half of every profile. + "2026-04-26T08:48:38.9980000Z", + "2026-04-26T08:48:38.998Z", "2026-08-06T04:56:19.555", // no offset at all - local "2026-08-06T04:56:19", // no offset, no fraction - local "2026-08-06T04:56", // minutes only - local diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt index 34438da1bcce..84742dd18fc7 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt @@ -139,6 +139,72 @@ class ProfileJsonCharacterizationTest : TestBase() { .isEqualTo(buildJsonObject { put("value", JsonPrimitive(0.825)) }.toString()) } + // ---------------------------------------------------------------- 5. what a real NS store looks like + + /** + * Shape facts taken from a live Nightscout `/api/v1/profile.json`, not from the NS docs. + * + * The response is an **array** of store documents (the profile history), each holding a `store` + * keyed by user-chosen profile name. Nightscout adds its own fields (`_id`, `date`, `srvModified`, + * `app`, `subject`, `identifier`, `utcOffset`) and omits the AAPS-specific ones entirely - a real + * NS-authored profile has no `dia` and no `iCfg`, so those must stay optional on the read path. + */ + @Test fun `a real nightscout store document parses`() { + val nsShaped = """ + [{"_id":"6a4507e64d1ba8c2e4196ec1","defaultProfile":"Vsedni den", + "date":1782908904415,"created_at":"2026-07-01T12:28:24.415Z", + "startDate":"2026-07-01T12:28:24.4150000Z","utcOffset":0, + "store":{"Vsedni den":{ + "carbratio":[{"time":"00:00","timeAsSeconds":0,"value":8.1}], + "sens":[{"time":"00:00","timeAsSeconds":0,"value":10}], + "basal":[{"time":"00:00","timeAsSeconds":0,"value":1}, + {"time":"06:00","timeAsSeconds":21600,"value":1.27}], + "target_low":[{"time":"00:00","timeAsSeconds":0,"value":5.5}], + "target_high":[{"time":"00:00","timeAsSeconds":0,"value":5.5}], + "units":"mmol","timezone":"Europe/Prague"}}}] + """.trimIndent() + + val doc = JSONArray(nsShaped).getJSONObject(0) + assertThat(doc.getString("defaultProfile")).isEqualTo("Vsedni den") + assertThat(doc.has("dia")).isFalse() + assertThat(doc.has("iCfg")).isFalse() + + val profile = doc.getJSONObject("store").getJSONObject("Vsedni den") + val basal = blockFromJsonArray(profile.getJSONArray("basal"), dateUtil) + assertThat(basal).isNotNull() + assertThat(basal!![0].amount).isEqualTo(1.0) + assertThat(basal[1].amount).isEqualTo(1.27) + } + + /** + * The one that decides the whole-number question. + * + * Nightscout itself writes a whole-numbered rate **bare** - the live document contains + * `"value":1`, `"value":6`, `"value":10`, never `1.0`. So `org.json`'s rendering matches what the + * server produces, and kotlinx's `1.0` would match neither AAPS today nor Nightscout. That makes + * the divergence worth shimming rather than accepting. + */ + @Test fun `nightscout writes whole numbered rates without a fraction`() { + val fromNs = """{"basal":[{"time":"00:00","timeAsSeconds":0,"value":1}]}""" + val entry = JSONObject(fromNs).getJSONArray("basal").getJSONObject(0) + + assertThat(entry.getDouble("value")).isEqualTo(1.0) + // round-tripping it through org.json keeps the bare form + assertThat(JSONObject().put("value", entry.getDouble("value")).toString()).isEqualTo("""{"value":1}""") + } + + /** + * Softens the slash finding: Nightscout stores and returns `Europe/Prague` unescaped, so the + * `mg\/dl` form org.json emits is normalised away server-side. It is still a byte difference on + * the way out, but it does not survive a round trip - worth knowing before treating it as a + * blocker. + */ + @Test fun `nightscout returns slashes unescaped`() { + val fromNs = """{"timezone":"Europe/Prague"}""" + assertThat(fromNs).doesNotContain("\\/") + assertThat(JSONObject(fromNs).getString("timezone")).isEqualTo("Europe/Prague") + } + // ---------------------------------------------------------------- 4. what strict parsing costs (less than expected) /** From 419e949a718b75d5e465dc6588aa50f17fbaa5b7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 18:42:44 +0200 Subject: [PATCH 060/146] Write side differences are benign, record why --- .../ProfileJsonCharacterizationTest.kt | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt index 84742dd18fc7..75145b046113 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt @@ -35,11 +35,14 @@ import org.mockito.Mock * 2. A malformed schedule makes `blockFromJsonArray` return **null** (an invalid profile), not throw. * Strict parsing would turn that into an exception, i.e. a crash where today there is a rejected * profile. - * 3. `org.json` writes a whole-numbered double without its fraction, so a basal of `1.0` is uploaded - * as `1`. kotlinx writes `1.0`. A full profile has ~120 such values. + * 3. The write side renders differently (`1` vs `1.0`, `mg\/dl` vs `mg/dl`) and it does NOT matter. + * That was investigated as a blocker and dismissed on evidence: `ProfileSealed.isEqual` compares + * decoded values hour by hour, no profile document is hashed or signed, and both forms decode to + * the same number or string. A rendering shim was planned and dropped. See section 3. * - * When the conversion happens these assertions must still hold. Where a divergence is intentional it - * is asserted as a divergence here, not quietly skipped. + * So the risk lives entirely on the READ side: tolerate both quoted and unquoted numbers, tolerate + * missing AAPS fields and unknown server fields, accept two different ISO shapes, and keep returning + * null - not throwing - on malformed input. Those are the assertions a conversion must not break. */ class ProfileJsonCharacterizationTest : TestBase() { @@ -114,26 +117,37 @@ class ProfileJsonCharacterizationTest : TestBase() { assertThat(blockFromJsonArray(null, dateUtil)).isNull() } - // ---------------------------------------------------------------- 3. the write side diverges + // -------------------------------------------- 3. the write side differs, and it does NOT matter /** - * The divergence that would change bytes uploaded to Nightscout. + * `org.json` renders a whole-numbered double bare (`1`), kotlinx keeps the fraction (`1.0`). * - * A profile has 24 basal + 24 ISF + 24 IC + 48 target entries, so a document can differ in ~120 - * places without a single value being wrong. Both parse back identically - what breaks is any - * consumer comparing documents as text. + * Recorded so nobody "fixes" it: this was investigated as a possible blocker and is not one. + * Nothing in AAPS or Nightscout observes the rendering, because nothing compares profile + * documents as text: + * + * - `ProfileSealed.isEqual` walks the DECODED values hour by hour + * (`getBasalTimeFromMidnight(...) != ...`), so `1` and `1.0` compare equal. + * - No profile document is hashed or signed anywhere; the only `getData().toString()` is a log + * line in `ProfileRepositoryImpl`. + * - Both sides parse either form to the same double, so the round trip is lossless. + * + * A number-rendering shim was planned and then dropped on that evidence. Do not add one without + * first finding a consumer that actually reads the text. */ - @Test fun `whole numbered profile values serialize differently`() { + @Test fun `whole numbered values render differently but nothing observes it`() { val org = JSONObject().put("value", 1.0).toString() val kotlinx = buildJsonObject { put("value", JsonPrimitive(1.0)) }.toString() assertThat(org).isEqualTo("""{"value":1}""") assertThat(kotlinx).isEqualTo("""{"value":1.0}""") - assertWithMessage("if this ever passes, the divergence is gone and the shim can drop it") - .that(org).isNotEqualTo(kotlinx) + + // The part that makes it benign: both decode to the same number. + assertThat(JSONObject(org).getDouble("value")).isEqualTo(1.0) + assertThat(JSONObject(kotlinx).getDouble("value")).isEqualTo(1.0) } - /** A fractional basal rate is written the same by both, so only the whole-numbered case matters. */ + /** A fractional basal rate is written identically by both - only the whole-numbered case differs at all. */ @Test fun `fractional profile values serialize identically`() { assertThat(JSONObject().put("value", 0.825).toString()) .isEqualTo(buildJsonObject { put("value", JsonPrimitive(0.825)) }.toString()) @@ -177,12 +191,12 @@ class ProfileJsonCharacterizationTest : TestBase() { } /** - * The one that decides the whole-number question. + * Nightscout writes a whole-numbered rate **bare** - the live document contains `"value":1`, + * `"value":6`, `"value":10`, never `1.0`. * - * Nightscout itself writes a whole-numbered rate **bare** - the live document contains - * `"value":1`, `"value":6`, `"value":10`, never `1.0`. So `org.json`'s rendering matches what the - * server produces, and kotlinx's `1.0` would match neither AAPS today nor Nightscout. That makes - * the divergence worth shimming rather than accepting. + * Kept as a shape fact, not as an argument for a shim. What matters is the line below: whatever + * the rendering, it reads back as the same double, and that is all any consumer of a profile + * does with it. */ @Test fun `nightscout writes whole numbered rates without a fraction`() { val fromNs = """{"basal":[{"time":"00:00","timeAsSeconds":0,"value":1}]}""" @@ -231,13 +245,12 @@ class ProfileJsonCharacterizationTest : TestBase() { } /** - * The divergence that only real data revealed: `org.json` escapes a forward slash, kotlinx does - * not. Every profile carries two slashed strings - `units` (`mg/dl`) and `timezone` - * (`Africa/Cairo`) - so this changes bytes on every single upload, independently of the - * whole-number issue above. + * `org.json` escapes a forward slash, kotlinx does not - `mg\/dl` versus `mg/dl`. * - * Both decode to the same string, so nothing inside AAPS notices. A document hash or a byte - * comparison on the Nightscout side would. + * Benign for the same reason as the whole-number case: both decode to the same string and nothing + * compares profile documents as text. Doubly so here, because Nightscout stores and returns the + * slash UNESCAPED (see the test below), so the difference does not even survive a round trip - + * `org.json` is the odd one out, not kotlinx. */ @Test fun `forward slashes are escaped by org json but not by kotlinx`() { val org = JSONObject().put("units", "mg/dl").toString() From fbc5449bb751b36472c2a88136d9ca69a1ea4660 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 18:48:42 +0200 Subject: [PATCH 061/146] Profile schedule parsing moves to kotlinx --- .../core/objects/extensions/BlockExtension.kt | 78 ++++++++++++++----- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt index 740dab3897c8..edb12e2b3d65 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt @@ -4,6 +4,10 @@ import app.aaps.core.data.model.data.Block import app.aaps.core.data.model.data.TargetBlock import app.aaps.core.data.time.T import app.aaps.core.interfaces.utils.DateUtil +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import org.json.JSONArray import org.json.JSONObject @@ -80,30 +84,64 @@ fun List.highTargetBlockValueBySeconds(secondsFromMidnight: Int, ti return last().highTarget } -fun blockFromJsonArray(jsonArray: JSONArray?, dateUtil: DateUtil): List? { - val size = jsonArray?.length() ?: return null - val ret = ArrayList(size) - try { - for (index in 0 until jsonArray.length() - 1) { - val o = jsonArray.getJSONObject(index) - val tas = dateUtil.toSeconds(o.getString("time")) - val next = jsonArray.getJSONObject(index + 1) - val nextTas = dateUtil.toSeconds(next.getString("time")) - val value = o.getDouble("value") - if (tas % 3600 != 0) return null - if (nextTas % 3600 != 0) return null - ret.add(index, Block((nextTas - tas) * 1000L, value)) - } - val last: JSONObject = jsonArray.getJSONObject(jsonArray.length() - 1) - val lastTas = dateUtil.toSeconds(last.getString("time")) - val value = last.getDouble("value") - ret.add(jsonArray.length() - 1, Block((T.hours(24).secs() - lastTas) * 1000L, value)) - } catch (e: Exception) { - return null +/** + * Bridge from `org.json` to kotlinx at the module boundary. + * + * Goes via text because that is the only lossless thing both libraries agree on, and the cost is + * paid once per schedule rather than per entry. Returns null rather than throwing so callers keep the + * existing "unreadable means invalid profile" behaviour. + */ +private fun JSONArray?.toKotlinxOrNull(): JsonArray? = + this?.let { runCatching { Json.parseToJsonElement(it.toString()) as? JsonArray }.getOrNull() } + +/** + * `time` as org.json's `getString` would give it: a quoted string comes back unquoted, and a bare + * number comes back as its text. Null when absent or not a primitive. + */ +private fun JsonArray.timeAt(index: Int): String? = + ((getOrNull(index) as? JsonObject)?.get("time") as? JsonPrimitive)?.content + +/** + * `value` as org.json's `getDouble` would give it - coercing a quoted number, which real Nightscout + * and AAPS documents both contain. Null when absent or not numeric, which the callers turn into an + * invalid profile. + */ +private fun JsonArray.valueAt(index: Int): Double? = + ((getOrNull(index) as? JsonObject)?.get("value") as? JsonPrimitive)?.content?.toDoubleOrNull() + +/** + * Reads a profile schedule (`basal`, `sens`, `carbratio`) into blocks. + * + * The logic lives on kotlinx [JsonArray] so it can eventually move to commonMain; the `org.json` + * entry point below is a thin adapter kept while the callers still hold `JSONArray`. This is the + * inside-out step of the org.json migration - the parsing rules move first, the contracts follow. + * + * Behaviour is deliberately unchanged and is pinned by `ProfileJsonCharacterizationTest`: + * - a value may be a real number OR a quoted string; both occur in the wild + * - a schedule not aligned to whole hours is rejected + * - anything unreadable yields **null** (an invalid profile), never an exception + */ +fun blockFromJson(jsonArray: JsonArray?, dateUtil: DateUtil): List? { + if (jsonArray == null || jsonArray.isEmpty()) return null + val ret = ArrayList(jsonArray.size) + for (index in 0 until jsonArray.size - 1) { + val tas = dateUtil.toSeconds(jsonArray.timeAt(index) ?: return null) + val nextTas = dateUtil.toSeconds(jsonArray.timeAt(index + 1) ?: return null) + val value = jsonArray.valueAt(index) ?: return null + if (tas % 3600 != 0) return null + if (nextTas % 3600 != 0) return null + ret.add(index, Block((nextTas - tas) * 1000L, value)) } + val lastTas = dateUtil.toSeconds(jsonArray.timeAt(jsonArray.size - 1) ?: return null) + val lastValue = jsonArray.valueAt(jsonArray.size - 1) ?: return null + ret.add(jsonArray.size - 1, Block((T.hours(24).secs() - lastTas) * 1000L, lastValue)) return ret } +/** `org.json` entry point. Converts once at the boundary and delegates to [blockFromJson]. */ +fun blockFromJsonArray(jsonArray: JSONArray?, dateUtil: DateUtil): List? = + blockFromJson(jsonArray.toKotlinxOrNull(), dateUtil) + fun targetBlockFromJsonArray(jsonArray1: JSONArray?, jsonArray2: JSONArray?, dateUtil: DateUtil): List? { val size1 = jsonArray1?.length() ?: return null val size2 = jsonArray2?.length() ?: return null From 9b215a842adc8e854f2651f15fbcc83195463115 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 19:10:09 +0200 Subject: [PATCH 062/146] Target range parsing moves to kotlinx --- .../core/objects/extensions/BlockExtension.kt | 65 ++++++++------ .../ProfileJsonCharacterizationTest.kt | 88 +++++++++++++++++++ 2 files changed, 124 insertions(+), 29 deletions(-) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt index edb12e2b3d65..8108b158c409 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt @@ -142,34 +142,41 @@ fun blockFromJson(jsonArray: JsonArray?, dateUtil: DateUtil): List? { fun blockFromJsonArray(jsonArray: JSONArray?, dateUtil: DateUtil): List? = blockFromJson(jsonArray.toKotlinxOrNull(), dateUtil) -fun targetBlockFromJsonArray(jsonArray1: JSONArray?, jsonArray2: JSONArray?, dateUtil: DateUtil): List? { - val size1 = jsonArray1?.length() ?: return null - val size2 = jsonArray2?.length() ?: return null - if (size1 != size2) return null - val ret = ArrayList(size1) - try { - for (index in 0 until jsonArray1.length() - 1) { - val o1: JSONObject = jsonArray1.getJSONObject(index) - val tas1 = dateUtil.toSeconds(o1.getString("time")) - val value1 = o1.getDouble("value") - val next1 = jsonArray1.getJSONObject(index + 1) - val nextTas1 = dateUtil.toSeconds(next1.getString("time")) - val o2 = jsonArray2.getJSONObject(index) - val tas2 = dateUtil.toSeconds(o2.getString("time")) - val value2 = o2.getDouble("value") - if (tas1 != tas2) return null - if (tas1 % 3600 != 0) return null - if (nextTas1 % 3600 != 0) return null - ret.add(index, TargetBlock((nextTas1 - tas1) * 1000L, value1, value2)) - } - val last1 = jsonArray1.getJSONObject(jsonArray1.length() - 1) - val lastTas1 = dateUtil.toSeconds(last1.getString("time")) - val value1 = last1.getDouble("value") - val last2 = jsonArray2.getJSONObject(jsonArray2.length() - 1) - val value2 = last2.getDouble("value") - ret.add(jsonArray1.length() - 1, TargetBlock((T.hours(24).secs() - lastTas1) * 1000L, value1, value2)) - } catch (e: Exception) { - return null +/** + * Reads `target_low` and `target_high` together into range blocks. + * + * Same inside-out treatment as [blockFromJson]. Rules pinned by `ProfileJsonCharacterizationTest`: + * the two arrays must be the same length, entry N of each must be for the same time, and every time + * must fall on the hour. + * + * The last entry's times are deliberately NOT compared - the original loop only checked entries + * 0..n-2 and read just the value of the final one. That is reproduced exactly rather than tightened, + * so this conversion stays provably behaviour-preserving; see + * `a differing time on the LAST entry is not caught`. + */ +fun targetBlockFromJson(jsonArray1: JsonArray?, jsonArray2: JsonArray?, dateUtil: DateUtil): List? { + if (jsonArray1 == null || jsonArray2 == null) return null + if (jsonArray1.isEmpty() || jsonArray1.size != jsonArray2.size) return null + val ret = ArrayList(jsonArray1.size) + for (index in 0 until jsonArray1.size - 1) { + val tas1 = dateUtil.toSeconds(jsonArray1.timeAt(index) ?: return null) + val value1 = jsonArray1.valueAt(index) ?: return null + val nextTas1 = dateUtil.toSeconds(jsonArray1.timeAt(index + 1) ?: return null) + val tas2 = dateUtil.toSeconds(jsonArray2.timeAt(index) ?: return null) + val value2 = jsonArray2.valueAt(index) ?: return null + if (tas1 != tas2) return null + if (tas1 % 3600 != 0) return null + if (nextTas1 % 3600 != 0) return null + ret.add(index, TargetBlock((nextTas1 - tas1) * 1000L, value1, value2)) } + val lastIndex = jsonArray1.size - 1 + val lastTas1 = dateUtil.toSeconds(jsonArray1.timeAt(lastIndex) ?: return null) + val lastValue1 = jsonArray1.valueAt(lastIndex) ?: return null + val lastValue2 = jsonArray2.valueAt(lastIndex) ?: return null + ret.add(lastIndex, TargetBlock((T.hours(24).secs() - lastTas1) * 1000L, lastValue1, lastValue2)) return ret -} \ No newline at end of file +} + +/** `org.json` entry point. Converts once at the boundary and delegates to [targetBlockFromJson]. */ +fun targetBlockFromJsonArray(jsonArray1: JSONArray?, jsonArray2: JSONArray?, dateUtil: DateUtil): List? = + targetBlockFromJson(jsonArray1.toKotlinxOrNull(), jsonArray2.toKotlinxOrNull(), dateUtil) \ No newline at end of file diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt index 75145b046113..61b031dc0e05 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileJsonCharacterizationTest.kt @@ -4,6 +4,7 @@ import android.content.Context import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.objects.extensions.blockFromJsonArray +import app.aaps.core.objects.extensions.targetBlockFromJsonArray import app.aaps.shared.impl.utils.DateUtilImpl import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat @@ -94,6 +95,93 @@ class ProfileJsonCharacterizationTest : TestBase() { assertThat(blocks!![0].amount).isEqualTo(6.0) } + // ------------------------------------------------- 1b. target ranges: two arrays read in lockstep + + /** + * `target_low` and `target_high` are separate arrays read together, which gives this parser two + * rules the single-schedule one does not have. Neither was covered by any test before this file, + * so they are pinned here first - the conversion must not quietly drop them. + */ + @Test fun `target ranges pair low and high by position`() { + val blocks = targetBlockFromJsonArray( + schedule("00:00" to "5.5", "12:00" to "6.0"), + schedule("00:00" to "7.0", "12:00" to "7.5"), + dateUtil + ) + + assertThat(blocks).isNotNull() + assertThat(blocks!!).hasSize(2) + assertThat(blocks[0].lowTarget).isEqualTo(5.5) + assertThat(blocks[0].highTarget).isEqualTo(7.0) + assertThat(blocks[1].lowTarget).isEqualTo(6.0) + assertThat(blocks[1].highTarget).isEqualTo(7.5) + } + + /** Rule 1: the two arrays must be the same length. */ + @Test fun `mismatched target array lengths are rejected`() { + assertThat( + targetBlockFromJsonArray( + schedule("00:00" to "5.5", "12:00" to "6.0"), + schedule("00:00" to "7.0"), + dateUtil + ) + ).isNull() + } + + /** Rule 2: entry N of low and entry N of high must be for the same time. */ + @Test fun `target arrays with differing times are rejected`() { + assertThat( + targetBlockFromJsonArray( + schedule("00:00" to "5.5", "08:00" to "6.0", "12:00" to "6.5"), + schedule("00:00" to "7.0", "09:00" to "7.5", "12:00" to "8.0"), + dateUtil + ) + ).isNull() + } + + /** + * ...but NOT on the final entry, and that is a gap rather than a decision. + * + * The loop compares `tas1 != tas2` for entries 0..n-2. The last entry is handled after the loop, + * where only `last2`'s **value** is read - its time is never looked at. So two arrays that + * disagree about when the final segment starts are accepted, and the resulting block pairs a low + * from one time with a high from another. + * + * Pinned as current behaviour, deliberately NOT fixed here: this file exists to make the org.json + * conversion provably behaviour-preserving, and tightening a validation rule at the same time + * would make any later regression impossible to attribute. In practice both AAPS and Nightscout + * write the two arrays with identical times, so it is latent rather than active. + */ + @Test fun `a differing time on the LAST entry is not caught`() { + val blocks = targetBlockFromJsonArray( + schedule("00:00" to "5.5", "12:00" to "6.0"), + schedule("00:00" to "7.0", "13:00" to "7.5"), + dateUtil + ) + + assertThat(blocks).isNotNull() + assertThat(blocks!!).hasSize(2) + // the high value came from the 13:00 entry, the duration from the 12:00 one + assertThat(blocks[1].lowTarget).isEqualTo(6.0) + assertThat(blocks[1].highTarget).isEqualTo(7.5) + } + + /** ...and the hour-alignment rule applies here too. */ + @Test fun `target ranges not aligned to whole hours are rejected`() { + assertThat( + targetBlockFromJsonArray( + schedule("00:30" to "5.5", "12:00" to "6.0"), + schedule("00:30" to "7.0", "12:00" to "7.5"), + dateUtil + ) + ).isNull() + } + + @Test fun `a null target array yields null`() { + assertThat(targetBlockFromJsonArray(null, schedule("00:00" to "7.0"), dateUtil)).isNull() + assertThat(targetBlockFromJsonArray(schedule("00:00" to "5.5"), null, dateUtil)).isNull() + } + // ---------------------------------------------------------------- 2. failure is null, not throw @Test fun `a non numeric value yields an invalid profile rather than an exception`() { From eb61bef6997a1bd649da8e784102e8f69b8e42ca Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 12 Aug 2026 20:38:26 +0200 Subject: [PATCH 063/146] Block and TargetBlock are immutable --- .../app/aaps/core/data/model/data/Block.kt | 2 +- .../aaps/core/data/model/data/TargetBlock.kt | 2 +- .../core/objects/extensions/BlockExtension.kt | 46 +++++++++++-------- .../core/objects/profile/ProfileSealed.kt | 9 +++- .../profile/ProfileFunctionImpl.kt | 8 ++-- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt index 7a009a8d6638..2325226735c6 100644 --- a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/Block.kt @@ -2,7 +2,7 @@ package app.aaps.core.data.model.data import kotlin.time.Duration.Companion.days -data class Block(var duration: Long, var amount: Double) +data class Block(val duration: Long, val amount: Double) fun List.checkSanity(): Boolean { var sum = 0L diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt index 24bed37e01c2..c152e33f190f 100644 --- a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/data/TargetBlock.kt @@ -2,7 +2,7 @@ package app.aaps.core.data.model.data import kotlin.time.Duration.Companion.days -data class TargetBlock(var duration: Long, var lowTarget: Double, var highTarget: Double) +data class TargetBlock(val duration: Long, val lowTarget: Double, val highTarget: Double) fun List.checkSanity(): Boolean { var sum = 0L diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt index 8108b158c409..cd620776954a 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt @@ -17,33 +17,41 @@ private fun getShiftedTimeSecs(originalSeconds: Int, timeShiftHours: Int): Int { return shiftedSeconds } +/** + * Expands to 24 one-hour blocks, then merges neighbours that carry the same value. + * + * Builds the merged list forward instead of mutating durations in place, so [Block] can stay + * immutable. Same output as before: consecutive equal-valued hours collapse into one block whose + * duration is their sum. + */ fun List.shiftBlock(multiplier: Double, timeShiftHours: Int): List { - val newList = arrayListOf() - for (hour in 0..23) newList.add(Block(1000L * 60 * 60, blockValueBySeconds(hour * 3600, multiplier, timeShiftHours))) - for (i in newList.indices.reversed()) { - if (i > 0) - if (newList[i].amount == newList[i - 1].amount) { - newList[i - 1].duration += newList[i].duration - newList.removeAt(i) - } + val hourly = (0..23).map { blockValueBySeconds(it * 3600, multiplier, timeShiftHours) } + val merged = ArrayList(hourly.size) + for (amount in hourly) { + val last = merged.lastOrNull() + if (last != null && last.amount == amount) merged[merged.size - 1] = last.copy(duration = last.duration + HOUR_MS) + else merged.add(Block(HOUR_MS, amount)) } - return newList + return merged } +/** Same merge, for the paired low/high target schedule. */ fun List.shiftTargetBlock(timeShiftHours: Int): List { - val newList = arrayListOf() - for (hour in 0..23) - newList.add(TargetBlock(1000L * 60 * 60, lowTargetBlockValueBySeconds(hour * 3600, timeShiftHours), highTargetBlockValueBySeconds(hour * 3600, timeShiftHours))) - for (i in newList.indices.reversed()) { - if (i > 0) - if (newList[i].lowTarget == newList[i - 1].lowTarget && newList[i].highTarget == newList[i - 1].highTarget) { - newList[i - 1].duration += newList[i].duration - newList.removeAt(i) - } + val hourly = (0..23).map { + lowTargetBlockValueBySeconds(it * 3600, timeShiftHours) to highTargetBlockValueBySeconds(it * 3600, timeShiftHours) } - return newList + val merged = ArrayList(hourly.size) + for ((low, high) in hourly) { + val last = merged.lastOrNull() + if (last != null && last.lowTarget == low && last.highTarget == high) + merged[merged.size - 1] = last.copy(duration = last.duration + HOUR_MS) + else merged.add(TargetBlock(HOUR_MS, low, high)) + } + return merged } +private const val HOUR_MS = 1000L * 60 * 60 + fun List.blockValueBySeconds(secondsFromMidnight: Int, multiplier: Double, timeShiftHours: Int): Double { var elapsed = 0L val shiftedSeconds = getShiftedTimeSecs(secondsFromMidnight, timeShiftHours) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index 8877df668589..ffca867254d9 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -235,6 +235,13 @@ sealed class ProfileSealed( * pump's time granularity (30-min vs full-hour). This gates profile *activation* and drives the * non-blocking "won't run on this pump" warning shown while editing/viewing — it never blocks * editing, storage or sync. + * + * Pure: it reports, it does not repair. It used to also clamp `basal.amount` to the pump limit in + * place, which made a "validity check" quietly rewrite the caller's profile. No caller ever used + * the clamped value - both build a throwaway [Pure] first and read only [Profile.ValidityCheck] - + * and the clamp `break`s after the first offending block, so it could not have produced a + * consistently corrected profile anyway. Removing it is what let [app.aaps.core.data.model.data.Block] + * become immutable. */ fun validatePump(from: String, pump: Pump, config: Config, rh: ResourceHelper, notificationManager: NotificationManager, sendNotifications: Boolean): Profile.ValidityCheck { val validityCheck = Profile.ValidityCheck() @@ -257,13 +264,11 @@ sealed class ProfileSealed( } // Check for minimal basal value if (basalAmount < description.basalMinimumRate) { - basal.amount = description.basalMinimumRate if (sendNotifications) sendBelowMinimumNotification(from, notificationManager, rh) validityCheck.isValid = false validityCheck.reasons.add(rh.gs(R.string.minimalbasalvaluereplaced, from)) break } else if (basalAmount > description.basalMaximumRate) { - basal.amount = description.basalMaximumRate if (sendNotifications) sendAboveMaximumNotification(from, notificationManager, rh) validityCheck.isValid = false validityCheck.reasons.add(rh.gs(R.string.maximumbasalvaluereplaced, from)) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt index 5dff17336efb..e5fb6c06bfa2 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt @@ -60,9 +60,11 @@ class ProfileFunctionImpl @Inject constructor( // profile — up to 30000 identical copies within one switch's window. Canonicalize by EPS id: the first // deep copy seen for an id is kept and every later second reuses it, so the whole window shares one // instance. The wrapper stays per-second (cheap, re-captures activePlugin.activeAPS exactly as before). - // Guarded by the [cache] monitor. Safe to share because the cached EPS profile is only ever READ - // (getBasal/getIsf/… derive fresh lists via shiftBlock) — the one in-place mutator, validatePump()'s - // basal clamp, runs solely on freshly built Pure/PS profiles, never on a getProfile() result. + // Guarded by the [cache] monitor. Safe to share because Block and TargetBlock are immutable and + // the accessors derive fresh lists via shiftBlock, so a shared profile cannot be altered by a + // reader. This used to rest on a convention instead - validatePump() clamped basal amounts in + // place, and sharing was only safe because that clamp happened to run on freshly built profiles. + // The clamp is gone and the types are `val`, so the guarantee is now the compiler's. @VisibleForTesting val canonicalEps = HashMap() From 01f4ddb60447d82e8f2244903c0789d758c0572f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 13:35:37 +0200 Subject: [PATCH 064/146] Profiles as Kotlin objects, JSON only at the storage and Nightscout edges --- .../interfaces/profile/ProfileRepository.kt | 26 +- .../core/interfaces/profile/PureProfile.kt | 9 +- .../core/interfaces/profile/SingleProfile.kt | 46 ++- .../core/objects/extensions/BlockExtension.kt | 123 ++++++- .../extensions/ProfileSwitchExtension.kt | 33 +- .../core/objects/profile/ProfileSealed.kt | 2 - .../objects/extensions/BlockRenderTest.kt | 266 +++++++++++++++ .../profile/ProfileRepositoryImpl.kt | 214 ++++++------ .../profile/ProfileRepositoryImplTest.kt | 64 +++- .../plugins/aps/autotune/AutotunePlugin.kt | 17 +- .../plugins/aps/autotune/AutotuneCoreTest.kt | 1 - .../plugins/aps/autotune/AutotunePrepTest.kt | 1 - .../aaps/shared/tests/TestBaseWithProfile.kt | 1 + .../viewmodels/ProfileEditorViewModel.kt | 314 ++++++++---------- .../viewmodels/ProfileEditorViewModelTest.kt | 79 ++++- .../ProfileManagementViewModelTest.kt | 10 +- 16 files changed, 817 insertions(+), 389 deletions(-) create mode 100644 core/objects/src/test/kotlin/app/aaps/core/objects/extensions/BlockRenderTest.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt index b2bcf17cfbd1..ebf5daf0fd33 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt @@ -21,11 +21,12 @@ interface ProfileRepository { * The current profile list. Emits whenever profiles are added, removed, cloned, * or replaced (e.g. by an NS push via [loadFromNs]). * - * **Mutability contract:** [SingleProfile] is a mutable holder (its `JSONArray` fields - * are mutable, and the list spine is a snapshot but the elements are shared). Callers - * that intend to edit a profile must [SingleProfile.deepClone] first; mutating an - * element from this flow corrupts repository state and will leak across other - * collectors. To commit an edited clone back, call [replace]. + * [SingleProfile] is immutable, so a profile taken from here can be held, compared and passed + * around freely. Edit with `copy()` and commit through [replace]. + * + * Because the elements compare structurally, a mutation that produces an identical list is + * **not** re-emitted. Collectors that must react to every mutation, not only to every change of + * content, should watch [revision] instead. */ val profiles: StateFlow> @@ -39,6 +40,19 @@ interface ProfileRepository { */ val profile: StateFlow + /** + * Counts mutations. Bumped once per completed mutation, after [profiles] and [profile] already + * hold the new state, so a collector here can read either `.value` and get the matching (or a + * newer) snapshot. + * + * This exists because [profiles] deduplicates: [SingleProfile] compares structurally, so + * re-storing an identical list produces no emit at all. Some actions are about the *event*, not + * the content — [reset] must reload the editor's working copy even when the stored profile turned + * out to be byte-identical to what the user was editing. Watching this instead of [profiles] + * keeps those working without giving up deduplication for everyone else. + */ + val revision: StateFlow + /** * Clone the profile at [index]. The clone is appended with " copy" suffixed to its name. * @@ -147,7 +161,7 @@ interface ProfileRepository { /** * Replace the profile at [index] with [profile] and persist. Used by the editor to commit - * a snapshot-edited profile back to the store. + * an edited profile back to the store. * * @return [Result.success] if the replacement was persisted, or [Result.failure] with * [IndexOutOfBoundsException] if [index] is no longer valid (e.g. profiles were diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt index 9b59106d1176..198c6e057da7 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt @@ -4,15 +4,16 @@ import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.ICfg import app.aaps.core.data.model.data.Block import app.aaps.core.data.model.data.TargetBlock -import org.json.JSONObject import java.util.TimeZone /** - * Pure profile like it's entered by user. Contains only data and it's serialized version in JSON + * Pure profile like it's entered by user. Contains only data. + * + * It used to carry the source JSON alongside the blocks. Nothing read it — every consumer worked + * from the blocks — while every construction paid to build it, so it is gone. Render on demand with + * `Profile.toPureNsJson` when JSON is actually needed. */ class PureProfile( - /** Source json data (must correspond to the rest of the profile) */ - var jsonObject: JSONObject, var basalBlocks: List, var isfBlocks: List, var icBlocks: List, diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt index b56fe163a5ea..28f45919267c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt @@ -1,35 +1,27 @@ package app.aaps.core.interfaces.profile -import org.json.JSONArray +import app.aaps.core.data.model.data.Block +import app.aaps.core.data.model.data.TargetBlock /** * One entry in the local profile list. * - * Pairs a [name] with profile data ([ic], [isf], [basal], target ranges) and the glucose - * unit flag ([mgdl]). The [JSONArray] fields hold the per-hour-block schedules in the same - * shape as Nightscout profile JSON, so they round-trip cleanly through the profile store. + * Pairs a [name] with profile data ([ic], [isf], [basal], [target]) and the glucose unit flag + * ([mgdl]). The schedules are plain Kotlin data: JSON exists only at the edges — the stored + * preference document and the Nightscout wire format — never in memory. * - * Use [deepClone] before mutating an instance you've received from [ProfileRepository] — - * the repository holds references and the `JSONArray`s are mutable. + * Immutable and structurally comparable. Edit with [copy]; there is nothing to clone defensively, + * so a profile handed out by [ProfileRepository] can be held and compared freely. + * + * Low and high targets live in ONE [TargetBlock] list rather than two parallel ones. The two JSON + * arrays must always have matching times and lengths, and pairing them here makes that a property of + * the type instead of something each reader has to re-check. */ -class SingleProfile( - var name: String, - var mgdl: Boolean, - var ic: JSONArray, - var isf: JSONArray, - var basal: JSONArray, - var targetLow: JSONArray, - var targetHigh: JSONArray, -) { - - fun deepClone(): SingleProfile = - SingleProfile( - name = name, - mgdl = mgdl, - ic = JSONArray(ic.toString()), - isf = JSONArray(isf.toString()), - basal = JSONArray(basal.toString()), - targetLow = JSONArray(targetLow.toString()), - targetHigh = JSONArray(targetHigh.toString()) - ) -} +data class SingleProfile( + val name: String, + val mgdl: Boolean, + val ic: List, + val isf: List, + val basal: List, + val target: List +) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt index cd620776954a..bd1d7ef0085d 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/BlockExtension.kt @@ -8,6 +8,9 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.json.JSONArray import org.json.JSONObject @@ -102,12 +105,41 @@ fun List.highTargetBlockValueBySeconds(secondsFromMidnight: Int, ti private fun JSONArray?.toKotlinxOrNull(): JsonArray? = this?.let { runCatching { Json.parseToJsonElement(it.toString()) as? JsonArray }.getOrNull() } +/** The shape [DateUtil.toSeconds] can actually read: ASCII digits, `HH:MM`. It matches with `find`. */ +private val READABLE_TIME = Regex("""\d+:\d+""") + /** - * `time` as org.json's `getString` would give it: a quoted string comes back unquoted, and a bare - * number comes back as its text. Null when absent or not a primitive. + * Start-of-block seconds for entry [index]. + * + * `timeAsSeconds` is the source of truth, and `time` is read only when it is missing or zero. That + * matches how the profile editor always worked - it wrote `time` but read the rows back from + * `timeAsSeconds`, never from `time`. + * + * Preferring the integer matters because `time` is the fragile field. Older builds formatted it with + * the device locale's digits, so a profile saved under ar-SA (a locale AAPS ships) holds `"٠٦:٠٠"`, + * and [DateUtil.toSeconds] matches ASCII `\d` only, answering 0 without complaining. That used to be + * survivable - the editor showed the right rows and the stored array was written back verbatim - but + * the document is now re-rendered from whatever is parsed here, so a `time` we cannot read would + * collapse every block to 00:00 and publish that to the sync channel and to Nightscout. The damage + * would be silent, because all-zero starts still divide evenly by 3600 and so still parse. + * + * Treating **zero** as "ask `time`" is what keeps this from failing the same way in reverse: an + * uploader that omits `timeAsSeconds` from a defaults-filled struct writes 0 for every entry, and + * trusting that would flatten the schedule just as badly. A real midnight block is unaffected - its + * `time` reads back as 0 anyway - and when neither field can be read, a present-but-zero + * `timeAsSeconds` is still honoured. + * + * Null only when neither field is usable, which the callers turn into an invalid profile. */ -private fun JsonArray.timeAt(index: Int): String? = - ((getOrNull(index) as? JsonObject)?.get("time") as? JsonPrimitive)?.content +private fun JsonArray.startSecondsAt(index: Int, dateUtil: DateUtil): Int? { + val entry = getOrNull(index) as? JsonObject + val seconds = (entry?.get("timeAsSeconds") as? JsonPrimitive)?.content?.toDoubleOrNull()?.toInt() + if (seconds != null && seconds != 0) return seconds + val time = (entry?.get("time") as? JsonPrimitive)?.content + if (time != null && READABLE_TIME.containsMatchIn(time)) return dateUtil.toSeconds(time) + // Absent -> null (invalid profile); present and zero -> a genuine midnight block. + return seconds +} /** * `value` as org.json's `getDouble` would give it - coercing a quoted number, which real Nightscout @@ -133,14 +165,14 @@ fun blockFromJson(jsonArray: JsonArray?, dateUtil: DateUtil): List? { if (jsonArray == null || jsonArray.isEmpty()) return null val ret = ArrayList(jsonArray.size) for (index in 0 until jsonArray.size - 1) { - val tas = dateUtil.toSeconds(jsonArray.timeAt(index) ?: return null) - val nextTas = dateUtil.toSeconds(jsonArray.timeAt(index + 1) ?: return null) + val tas = jsonArray.startSecondsAt(index, dateUtil) ?: return null + val nextTas = jsonArray.startSecondsAt(index + 1, dateUtil) ?: return null val value = jsonArray.valueAt(index) ?: return null if (tas % 3600 != 0) return null if (nextTas % 3600 != 0) return null ret.add(index, Block((nextTas - tas) * 1000L, value)) } - val lastTas = dateUtil.toSeconds(jsonArray.timeAt(jsonArray.size - 1) ?: return null) + val lastTas = jsonArray.startSecondsAt(jsonArray.size - 1, dateUtil) ?: return null val lastValue = jsonArray.valueAt(jsonArray.size - 1) ?: return null ret.add(jsonArray.size - 1, Block((T.hours(24).secs() - lastTas) * 1000L, lastValue)) return ret @@ -167,10 +199,10 @@ fun targetBlockFromJson(jsonArray1: JsonArray?, jsonArray2: JsonArray?, dateUtil if (jsonArray1.isEmpty() || jsonArray1.size != jsonArray2.size) return null val ret = ArrayList(jsonArray1.size) for (index in 0 until jsonArray1.size - 1) { - val tas1 = dateUtil.toSeconds(jsonArray1.timeAt(index) ?: return null) + val tas1 = jsonArray1.startSecondsAt(index, dateUtil) ?: return null val value1 = jsonArray1.valueAt(index) ?: return null - val nextTas1 = dateUtil.toSeconds(jsonArray1.timeAt(index + 1) ?: return null) - val tas2 = dateUtil.toSeconds(jsonArray2.timeAt(index) ?: return null) + val nextTas1 = jsonArray1.startSecondsAt(index + 1, dateUtil) ?: return null + val tas2 = jsonArray2.startSecondsAt(index, dateUtil) ?: return null val value2 = jsonArray2.valueAt(index) ?: return null if (tas1 != tas2) return null if (tas1 % 3600 != 0) return null @@ -178,7 +210,7 @@ fun targetBlockFromJson(jsonArray1: JsonArray?, jsonArray2: JsonArray?, dateUtil ret.add(index, TargetBlock((nextTas1 - tas1) * 1000L, value1, value2)) } val lastIndex = jsonArray1.size - 1 - val lastTas1 = dateUtil.toSeconds(jsonArray1.timeAt(lastIndex) ?: return null) + val lastTas1 = jsonArray1.startSecondsAt(lastIndex, dateUtil) ?: return null val lastValue1 = jsonArray1.valueAt(lastIndex) ?: return null val lastValue2 = jsonArray2.valueAt(lastIndex) ?: return null ret.add(lastIndex, TargetBlock((T.hours(24).secs() - lastTas1) * 1000L, lastValue1, lastValue2)) @@ -187,4 +219,71 @@ fun targetBlockFromJson(jsonArray1: JsonArray?, jsonArray2: JsonArray?, dateUtil /** `org.json` entry point. Converts once at the boundary and delegates to [targetBlockFromJson]. */ fun targetBlockFromJsonArray(jsonArray1: JSONArray?, jsonArray2: JSONArray?, dateUtil: DateUtil): List? = - targetBlockFromJson(jsonArray1.toKotlinxOrNull(), jsonArray2.toKotlinxOrNull(), dateUtil) \ No newline at end of file + targetBlockFromJson(jsonArray1.toKotlinxOrNull(), jsonArray2.toKotlinxOrNull(), dateUtil) + +/** + * `HH:00` for a whole-hour offset from midnight. + * + * Padded by hand rather than with `String.format`: `%02d` renders digits using the *locale's* zero + * digit, so under a locale such as ar-EG it writes `٠١:٠٠`, which no reader can parse back. The + * profile editor used to build times that way, so this also removes that trap. + */ +private fun hourLabel(secondsFromMidnight: Int): String { + val hours = secondsFromMidnight / 3600 + return (if (hours < 10) "0$hours" else "$hours") + ":00" +} + +/** + * Render a schedule back to the `[{time, timeAsSeconds, value}, …]` array shape used by both + * Nightscout and the stored profile document. + * + * Blocks carry durations, not start times, so each entry's start is the running sum of the blocks + * before it. Inverse of [blockFromJson] for any schedule that one accepts. + */ +fun List.toJsonArray(): JsonArray { + var startSeconds = 0 + return buildJsonArray { + for (block in this@toJsonArray) { + add(timeValueObject(startSeconds, block.amount)) + startSeconds += T.msecs(block.duration).secs().toInt() + } + } +} + +/** [toJsonArray] for the low side of a target schedule. */ +fun List.lowToJsonArray(): JsonArray = toJsonArray { it.lowTarget } + +/** [toJsonArray] for the high side of a target schedule. */ +fun List.highToJsonArray(): JsonArray = toJsonArray { it.highTarget } + +private fun List.toJsonArray(select: (TargetBlock) -> Double): JsonArray { + var startSeconds = 0 + return buildJsonArray { + for (block in this@toJsonArray) { + add(timeValueObject(startSeconds, select(block))) + startSeconds += T.msecs(block.duration).secs().toInt() + } + } +} + +private fun timeValueObject(startSeconds: Int, value: Double): JsonObject = + buildJsonObject { + put("time", hourLabel(startSeconds)) + put("timeAsSeconds", startSeconds) + put("value", value) + } + +/** `org.json` adapter for [toJsonArray], mirroring [blockFromJsonArray] on the read side. */ +fun List.toJSONArray(): JSONArray = JSONArray(toJsonArray().toString()) + +/** `org.json` adapter for [lowToJsonArray]. */ +fun List.lowToJSONArray(): JSONArray = JSONArray(lowToJsonArray().toString()) + +/** `org.json` adapter for [highToJsonArray]. */ +fun List.highToJSONArray(): JSONArray = JSONArray(highToJsonArray().toString()) + +/** One block covering the whole day — the shape a freshly seeded or damaged schedule takes. */ +fun singleBlock(value: Double): List = listOf(Block(T.hours(24).msecs(), value)) + +/** [singleBlock] for a target schedule. */ +fun singleTargetBlock(low: Double, high: Double): List = listOf(TargetBlock(T.hours(24).msecs(), low, high)) \ No newline at end of file diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt index 04cb8d77a202..5241577c1882 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt @@ -28,22 +28,24 @@ fun PS.getCustomizedName(decimalFormatter: DecimalFormatter): String { } /** - * Convert a [SingleProfile] to a [PureProfile] for graph rendering, validation, or - * activation. Single source of truth for the JSON-build pattern that was previously - * duplicated across ProfileManagementViewModel and ProfileEditorViewModel. + * Convert a [SingleProfile] to a [PureProfile] for graph rendering, validation, or activation. + * + * Both types now hold the same block lists, so this is a field copy. It used to render the profile + * to JSON and parse it straight back, which is why the editor paid for a full serialise/parse round + * trip on every keystroke. + * + * Nullable only to keep the call sites unchanged — a [SingleProfile] always carries usable blocks, + * so this never actually returns null. */ -fun SingleProfile.toPureProfile(dateUtil: DateUtil): PureProfile? { - val json = JSONObject().apply { - put("carbratio", ic) - put("sens", isf) - put("basal", basal) - put("target_low", targetLow) - put("target_high", targetHigh) - put("units", if (mgdl) GlucoseUnit.MGDL.asText else GlucoseUnit.MMOL.asText) - put("timezone", TimeZone.getDefault().id) - } - return pureProfileFromJson(json, dateUtil) -} +fun SingleProfile.toPureProfile(dateUtil: DateUtil): PureProfile? = + PureProfile( + basalBlocks = basal, + isfBlocks = isf, + icBlocks = ic, + targetBlocks = target, + glucoseUnit = if (mgdl) GlucoseUnit.MGDL else GlucoseUnit.MMOL, + timeZone = TimeZone.getDefault() + ) /** * Pure profile doesn't contain timestamp, percentage, timeshift, profileName @@ -66,7 +68,6 @@ fun pureProfileFromJson(jsonObject: JSONObject, dateUtil: DateUtil, defaultUnits ?: return null return PureProfile( - jsonObject = jsonObject, basalBlocks = basalBlocks, isfBlocks = isfBlocks, icBlocks = icBlocks, diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index ffca867254d9..a223eafc35a9 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -372,7 +372,6 @@ sealed class ProfileSealed( override fun convertToNonCustomizedProfile(dateUtil: DateUtil): PureProfile = PureProfile( - jsonObject = toPureNsJson(dateUtil), basalBlocks = basalBlocks.shiftBlock(percentage / 100.0, timeshift), isfBlocks = isfBlocks.shiftBlock(100.0 / percentage, timeshift), icBlocks = icBlocks.shiftBlock(100.0 / percentage, timeshift), @@ -510,7 +509,6 @@ sealed class ProfileSealed( if (this is EffectiveProfile) PP( PureProfile( - jsonObject = JSONObject(), basalBlocks = basalBlocks.shiftBlock(percentage / 100.0 / iCfg.concentration, timeshift), isfBlocks = isfBlocks.shiftBlock(100.0 / percentage * iCfg.concentration, timeshift), icBlocks = icBlocks.shiftBlock(100.0 / percentage * iCfg.concentration, timeshift), diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/BlockRenderTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/BlockRenderTest.kt new file mode 100644 index 000000000000..561c5ee8f202 --- /dev/null +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/BlockRenderTest.kt @@ -0,0 +1,266 @@ +package app.aaps.core.objects.extensions + +import app.aaps.core.data.model.data.Block +import app.aaps.core.data.model.data.TargetBlock +import app.aaps.core.data.time.T +import app.aaps.core.interfaces.utils.DateUtil +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.json.JSONArray +import java.util.Locale + +/** + * Pins the block → JSON renderers. + * + * These author two formats AAPS does not own alone: the profile document that master and client + * exchange (and that a 3.4.x build must still be able to read), and the Nightscout profile store. A + * renderer that drifted from [blockFromJson] would not fail loudly — profiles would round-trip to + * slightly different times or values, and the first sign would be a wrong basal rate. + * + * So the central property here is **round-trip identity**: rendering a schedule and parsing it back + * must return the same blocks, for every schedule the parser accepts. + */ +class BlockRenderTest { + + private val dateUtil: DateUtil = mock() + private lateinit var originalLocale: Locale + + @BeforeEach fun setUp() { + // toSeconds() is the real parser's job; here it only has to agree with what we render. + whenever(dateUtil.toSeconds(org.mockito.kotlin.any())).thenAnswer { invocation -> + val text = invocation.getArgument(0) + text.substringBefore(':').toInt() * 3600 + text.substringAfter(':').toInt() * 60 + } + originalLocale = Locale.getDefault() + } + + @AfterEach fun restoreLocale() { + Locale.setDefault(originalLocale) + } + + private fun blocks(vararg hoursAndValue: Pair): List = + hoursAndValue.map { (hours, value) -> Block(T.hours(hours.toLong()).msecs(), value) } + + private fun targets(vararg hoursLowHigh: Triple): List = + hoursLowHigh.map { (hours, low, high) -> TargetBlock(T.hours(hours.toLong()).msecs(), low, high) } + + @Test + fun `a whole-day single block renders as one 00-00 entry`() { + val json = singleBlock(0.1).toJSONArray() + + assertThat(json.length()).isEqualTo(1) + assertThat(json.getJSONObject(0).getString("time")).isEqualTo("00:00") + assertThat(json.getJSONObject(0).getInt("timeAsSeconds")).isEqualTo(0) + assertThat(json.getJSONObject(0).getDouble("value")).isEqualTo(0.1) + } + + @Test + fun `start times are the running sum of the durations before them`() { + val json = blocks(1 to 1.0, 2 to 2.0, 21 to 3.0).toJSONArray() + + assertThat((0 until json.length()).map { json.getJSONObject(it).getString("time") }) + .containsExactly("00:00", "01:00", "03:00").inOrder() + assertThat((0 until json.length()).map { json.getJSONObject(it).getInt("timeAsSeconds") }) + .containsExactly(0, 3600, 10800).inOrder() + } + + @Test + fun `hours past nine keep two digits`() { + val json = blocks(10 to 1.0, 14 to 2.0).toJSONArray() + + assertThat(json.getJSONObject(1).getString("time")).isEqualTo("10:00") + } + + /** + * `String.format("%02d")` renders digits with the *locale's* zero digit, so under a locale like + * ar-EG it produces "٠١:٠٠" — which no reader parses back. The editor used to build times that + * way. Rendering must not depend on the device locale at all. + */ + @Test + fun `times are ASCII regardless of the device locale`() { + Locale.setDefault(Locale.forLanguageTag("ar-EG")) + + val json = blocks(1 to 1.0, 23 to 2.0).toJSONArray() + + assertThat(json.getJSONObject(1).getString("time")).isEqualTo("01:00") + } + + @Test + fun `a rendered schedule parses back to the same blocks`() { + val cases = listOf( + singleBlock(0.1), + blocks(1 to 1.0, 23 to 2.0), + blocks(1 to 1.0, 2 to 2.0, 21 to 3.0), + List(24) { Block(T.hours(1).msecs(), it * 0.05) } + ) + + cases.forEach { original -> + assertWithMessage("round trip of %s", original) + .that(blockFromJsonArray(original.toJSONArray(), dateUtil)).isEqualTo(original) + } + } + + @Test + fun `a rendered target schedule parses back to the same blocks`() { + val cases = listOf( + singleTargetBlock(110.0, 120.0), + targets(Triple(6, 100.0, 110.0), Triple(18, 105.0, 115.0)), + List(24) { TargetBlock(T.hours(1).msecs(), 100.0 + it, 120.0 + it) } + ) + + cases.forEach { original -> + val parsed = targetBlockFromJsonArray(original.lowToJSONArray(), original.highToJSONArray(), dateUtil) + assertWithMessage("round trip of %s", original).that(parsed).isEqualTo(original) + } + } + + /** Low and high are rendered as two arrays but must stay aligned entry for entry. */ + @Test + fun `low and high render to matching times`() { + val target = targets(Triple(6, 100.0, 110.0), Triple(18, 105.0, 115.0)) + + val low = target.lowToJSONArray() + val high = target.highToJSONArray() + + assertThat(low.length()).isEqualTo(high.length()) + for (i in 0 until low.length()) { + assertThat(high.getJSONObject(i).getString("time")).isEqualTo(low.getJSONObject(i).getString("time")) + } + assertThat(low.getJSONObject(1).getDouble("value")).isEqualTo(105.0) + assertThat(high.getJSONObject(1).getDouble("value")).isEqualTo(115.0) + } + + /** + * The shape a 3.4.x build wrote and still has to be able to read: exactly these three fields, + * `time` as `HH:MM`. Adding or renaming one would be invisible to every test above, which only + * check that our own reader agrees with our own writer. + */ + @Test + fun `an entry carries exactly time, timeAsSeconds and value`() { + val entry = blocks(24 to 0.5).toJSONArray().getJSONObject(0) + + assertThat(entry.keys().asSequence().toList()).containsExactly("time", "timeAsSeconds", "value") + } + + @Test + fun `an empty schedule renders as an empty array rather than failing`() { + assertThat(emptyList().toJSONArray().length()).isEqualTo(0) + assertThat(emptyList().lowToJSONArray().length()).isEqualTo(0) + } + + /** + * A profile saved by an older build under a locale with non-ASCII digits. + * + * `String.format("%02d:00")` wrote the locale's own digits, and `DateUtil.toSeconds` matches + * ASCII `\d` only, so it silently answers 0 for every entry. Nothing downstream notices: all-zero + * start times still divide evenly by 3600, so the parse "succeeds" and returns blocks of zero + * duration. Since the stored document is now re-rendered from these blocks, reading such a `time` + * would flatten the whole schedule to 00:00 and publish that to the sync channel and to + * Nightscout. Leading with `timeAsSeconds` makes the broken text irrelevant. + */ + @Test + fun `a time written with non-ASCII digits is harmless because timeAsSeconds leads`() { + val arabicDigits = JSONArray( + """[{"time":"٠٠:٠٠","timeAsSeconds":0,"value":0.6}, + {"time":"٠٦:٠٠","timeAsSeconds":21600,"value":1.2}, + {"time":"٢٠:٠٠","timeAsSeconds":72000,"value":0.9}]""" + ) + + val parsed = blockFromJsonArray(arabicDigits, dateUtil) + + assertThat(parsed).isEqualTo( + listOf( + Block(T.hours(6).msecs(), 0.6), + Block(T.hours(14).msecs(), 1.2), + Block(T.hours(4).msecs(), 0.9) + ) + ) + } + + /** The same recovery for the paired target arrays. */ + @Test + fun `non-ASCII target times are harmless too`() { + val low = JSONArray("""[{"time":"٠٠:٠٠","timeAsSeconds":0,"value":100},{"time":"٠٦:٠٠","timeAsSeconds":21600,"value":105}]""") + val high = JSONArray("""[{"time":"٠٠:٠٠","timeAsSeconds":0,"value":110},{"time":"٠٦:٠٠","timeAsSeconds":21600,"value":115}]""") + + assertThat(targetBlockFromJsonArray(low, high, dateUtil)).isEqualTo( + listOf(TargetBlock(T.hours(6).msecs(), 100.0, 110.0), TargetBlock(T.hours(18).msecs(), 105.0, 115.0)) + ) + } + + /** + * `timeAsSeconds` is the source of truth, so a non-zero one wins over a readable but disagreeing + * `time`. This is the field the editor has always read its rows back from. + */ + @Test + fun `a non-zero timeAsSeconds wins over a disagreeing time`() { + val conflicting = JSONArray( + """[{"time":"00:00","timeAsSeconds":0,"value":1.0}, + {"time":"06:00","timeAsSeconds":28800,"value":2.0}]""" + ) + + // 28800 = 08:00, not the 06:00 the text claims. + assertThat(blockFromJsonArray(conflicting, dateUtil)) + .isEqualTo(listOf(Block(T.hours(8).msecs(), 1.0), Block(T.hours(16).msecs(), 2.0))) + } + + /** + * The mirror of the ar-SA bug, and the reason zero means "ask `time`". + * + * An uploader that omits `timeAsSeconds` from a defaults-filled struct writes 0 on every entry. + * Trusting that blindly would flatten the whole schedule to midnight - exactly the failure the + * fallback exists to prevent, just from the other direction. + */ + @Test + fun `an all-zero timeAsSeconds falls back to time rather than flattening`() { + val zeroed = JSONArray( + """[{"time":"00:00","timeAsSeconds":0,"value":0.6}, + {"time":"06:00","timeAsSeconds":0,"value":1.2}, + {"time":"20:00","timeAsSeconds":0,"value":0.9}]""" + ) + + assertThat(blockFromJsonArray(zeroed, dateUtil)).isEqualTo( + listOf( + Block(T.hours(6).msecs(), 0.6), + Block(T.hours(14).msecs(), 1.2), + Block(T.hours(4).msecs(), 0.9) + ) + ) + } + + /** A genuine midnight block is zero in both fields and must survive the fallback intact. */ + @Test + fun `a real midnight block still reads as zero`() { + val parsed = blockFromJsonArray(JSONArray("""[{"time":"00:00","timeAsSeconds":0,"value":0.7}]"""), dateUtil) + + assertThat(parsed).isEqualTo(listOf(Block(T.hours(24).msecs(), 0.7))) + } + + /** Zero seconds with an unreadable time is still a midnight block, not a rejection. */ + @Test + fun `zero seconds with an unreadable time is honoured`() { + val parsed = blockFromJsonArray(JSONArray("""[{"time":"٠٠:٠٠","timeAsSeconds":0,"value":0.7}]"""), dateUtil) + + assertThat(parsed).isEqualTo(listOf(Block(T.hours(24).msecs(), 0.7))) + } + + /** With neither field usable there is nothing to recover, so the profile is still invalid. */ + @Test + fun `an unreadable time and no timeAsSeconds is still rejected`() { + assertThat(blockFromJsonArray(JSONArray("""[{"time":"٠٦:٠٠","value":1.0}]"""), dateUtil)).isNull() + assertThat(blockFromJsonArray(JSONArray("""[{"value":1.0}]"""), dateUtil)).isNull() + } + + /** Guards the adapter itself: the kotlinx form and the org.json form must not diverge. */ + @Test + fun `the org json adapter matches the kotlinx renderer`() { + val schedule = blocks(1 to 1.0, 23 to 2.0) + + assertThat(JSONArray(schedule.toJsonArray().toString()).toString()).isEqualTo(schedule.toJSONArray().toString()) + } +} diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt index 5b5b3f87b2de..c28c8b1c0fae 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt @@ -3,6 +3,7 @@ package app.aaps.implementation.profile import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.data.Block +import app.aaps.core.data.model.data.TargetBlock import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger @@ -29,6 +30,12 @@ import app.aaps.core.keys.ProfileIntKey import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.blockFromJsonArray +import app.aaps.core.objects.extensions.highToJSONArray +import app.aaps.core.objects.extensions.lowToJSONArray +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock +import app.aaps.core.objects.extensions.targetBlockFromJsonArray +import app.aaps.core.objects.extensions.toJSONArray import app.aaps.core.objects.extensions.toPureProfile import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.R @@ -104,6 +111,9 @@ class ProfileRepositoryImpl @Inject constructor( private val _profile = MutableStateFlow(null) override val profile: StateFlow = _profile.asStateFlow() + private val _revision = MutableStateFlow(0L) + override val revision: StateFlow = _revision.asStateFlow() + init { // Synchronous initial load. Dagger constructs this @Singleton before any other // coroutine can run, so no mutex is needed here. Once init returns, the StateFlow @@ -153,15 +163,21 @@ class ProfileRepositoryImpl @Inject constructor( * mutation, inside the mutex. Reads `profilesList` / `rawProfile` (which are stable * under the lock) and publishes immutable views. * - * Order matters: `_profile` is written first, then `_profiles`. At least one current - * subscriber (ProfileManagementViewModel in the UI module) reads `profile.value` inside - * a `profiles` collector — writing the by-name JSON projection first means that when + * Order matters: `_profile` is written first, then `_profiles`, and `_revision` last. At least + * one current subscriber (ProfileManagementViewModel in the UI module) reads `profile.value` + * inside a `profiles` collector — writing the by-name JSON projection first means that when * the list emit fires, the store the collector reads is the new one (or newer), never * older. Subscribers that read only one flow are unaffected by ordering. + * + * `_revision` goes last for the same reason one step further out: it is the "something happened" + * signal, so by the time it fires both data flows must already carry the new state. It also + * always emits, while `_profiles` deduplicates identical lists — see + * [app.aaps.core.interfaces.profile.ProfileRepository.revision]. */ private fun snapshot() { _profile.value = rawProfile _profiles.value = profilesList.toList() + _revision.value = _revision.value + 1 } // --------------------------------------------------------------------------------------------- @@ -175,9 +191,8 @@ class ProfileRepositoryImpl @Inject constructor( } runCatching { withContext(Dispatchers.IO) { - val p = profilesList[index].deepClone() - p.name += " copy" - profilesList.add(p) + val original = profilesList[index] + profilesList.add(original.copy(name = original.name + " copy")) storeSettingsInternal(timestamp = dateUtil.now()) } snapshot() @@ -238,10 +253,9 @@ class ProfileRepositoryImpl @Inject constructor( } runCatching { withContext(Dispatchers.IO) { - // Defensive clone: the caller may continue to hold the reference (the editor - // keeps editingProfile alive after save). Storing the live reference would let - // subsequent caller-side mutations leak directly into profilesList[index]. - profilesList[index] = profile.deepClone() + // No defensive clone needed: SingleProfile is immutable, so the editor keeping its + // reference alive after save cannot reach into profilesList. + profilesList[index] = profile storeSettingsInternal(timestamp = dateUtil.now()) } snapshot() @@ -259,10 +273,10 @@ class ProfileRepositoryImpl @Inject constructor( override suspend fun loadFromNs(store: ProfileStore): Result = mutex.withLock { runCatching { - withContext(Dispatchers.IO) { - loadFromStoreInternal(store) - } - snapshot() + // Snapshot only when the store was actually taken. A rejected store changes nothing, and + // [revision] means "a mutation happened" — bumping it anyway would tell the editor to + // reload and would throw away whatever the user was typing at that moment. + if (withContext(Dispatchers.IO) { loadFromStoreInternal(store) }) snapshot() } } @@ -278,43 +292,29 @@ class ProfileRepositoryImpl @Inject constructor( if (name.isEmpty()) { errors.add(ProfileValidationError(ProfileErrorType.NAME, rh.gs(R.string.missing_profile_name))) } - // A block list is invalid when it cannot be read, when it holds no block at all, or when - // *any* single block is out of range. Two traps this avoids: "all blocks out of range" - // lets one bad block hide between good ones, and a null list must not count as valid. - fun invalid(blocks: List?, inRange: (Double) -> Boolean): Boolean = - blocks.isNullOrEmpty() || blocks.any { !inRange(it.amount) } + // A schedule is invalid when it holds no block at all, or when *any* single block is out + // of range. The "any" matters: checking only the first block would let one bad block hide + // between good ones. + fun invalid(blocks: List, inRange: (Double) -> Boolean): Boolean = + blocks.isEmpty() || blocks.any { !inRange(it.amount) } // BG values are stored in the profile unit, the limits are always mg/dL. fun asMgdl(value: Double): Double = if (mgdl) value else profileUtil.convertToMgdl(value, GlucoseUnit.MMOL) - val low = blockFromJsonArray(targetLow, dateUtil) - val high = blockFromJsonArray(targetHigh, dateUtil) + fun targetError() = ProfileValidationError(ProfileErrorType.TARGET, rh.gs(R.string.error_in_target_values)) - if (invalid(blockFromJsonArray(ic, dateUtil)) { it in hardLimits.icRange() }) { + if (invalid(ic) { it in hardLimits.icRange() }) { errors.add(ProfileValidationError(ProfileErrorType.IC, rh.gs(R.string.error_in_ic_values))) } - if (invalid(blockFromJsonArray(isf, dateUtil)) { asMgdl(it) in HardLimits.LIMIT_ISF }) { + if (invalid(isf) { asMgdl(it) in HardLimits.LIMIT_ISF }) { errors.add(ProfileValidationError(ProfileErrorType.ISF, rh.gs(R.string.error_in_isf_values))) } - if (invalid(blockFromJsonArray(basal, dateUtil)) { it in 0.01..hardLimits.maxBasal() }) { + if (invalid(basal) { it in 0.01..hardLimits.maxBasal() }) { errors.add(ProfileValidationError(ProfileErrorType.BASAL, rh.gs(R.string.error_in_basal_values))) } - if (invalid(low) { asMgdl(it) in HardLimits.LIMIT_MIN_BG }) { - errors.add(ProfileValidationError(ProfileErrorType.TARGET, rh.gs(R.string.error_in_target_values))) - } - if (invalid(high) { asMgdl(it) in HardLimits.LIMIT_MAX_BG }) { - errors.add(ProfileValidationError(ProfileErrorType.TARGET, rh.gs(R.string.error_in_target_values))) - } - low?.let { lowList -> - high?.let { highList -> - for (i in lowList.indices) { - if (lowList[i].amount > highList[i].amount) { - errors.add(ProfileValidationError(ProfileErrorType.TARGET, rh.gs(R.string.error_in_target_values))) - break - } - } - } - } + if (target.isEmpty() || target.any { asMgdl(it.lowTarget) !in HardLimits.LIMIT_MIN_BG }) errors.add(targetError()) + if (target.isEmpty() || target.any { asMgdl(it.highTarget) !in HardLimits.LIMIT_MAX_BG }) errors.add(targetError()) + if (target.any { it.lowTarget > it.highTarget }) errors.add(targetError()) if (name.contains(".")) { errors.add(ProfileValidationError(ProfileErrorType.NAME, rh.gs(R.string.profile_name_contains_dot))) } @@ -341,11 +341,10 @@ class ProfileRepositoryImpl @Inject constructor( return SingleProfile( name = Constants.LOCAL_PROFILE + free, mgdl = profileFunction.get().getUnits() == GlucoseUnit.MGDL, - ic = singleBlockProfileArray(0.0), - isf = singleBlockProfileArray(0.0), - basal = singleBlockProfileArray(0.0), - targetLow = singleBlockProfileArray(0.0), - targetHigh = singleBlockProfileArray(0.0) + ic = singleBlock(0.0), + isf = singleBlock(0.0), + basal = singleBlock(0.0), + target = singleTargetBlock(0.0, 0.0) ) } @@ -358,16 +357,15 @@ class ProfileRepositoryImpl @Inject constructor( if (_profile.value?.getSpecificProfile(newName) != null) { verifiedName += " " + dateUtil.now().toString() } - val profile = ProfileSealed.Pure(pureProfile, activePlugin) - val pureJson = pureProfile.jsonObject + // Straight field copy — a PureProfile already holds the same block lists, so there is no + // JSON to unpack any more. return SingleProfile( name = verifiedName, - mgdl = profile.units == GlucoseUnit.MGDL, - ic = pureJson.getJSONArray("carbratio"), - isf = pureJson.getJSONArray("sens"), - basal = pureJson.getJSONArray("basal"), - targetLow = pureJson.getJSONArray("target_low"), - targetHigh = pureJson.getJSONArray("target_high") + mgdl = pureProfile.glucoseUnit == GlucoseUnit.MGDL, + ic = pureProfile.icBlocks, + isf = pureProfile.isfBlocks, + basal = pureProfile.basalBlocks, + target = pureProfile.targetBlocks ) } @@ -420,15 +418,20 @@ class ProfileRepositoryImpl @Inject constructor( val name = preferences.get(ProfileComposedStringKey.LocalProfileNumberedName, i) if (profilesList.any { it.name == name }) continue try { + val entry = JSONObject() + .put(KEY_IC, JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIc, i))) + .put(KEY_ISF, JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIsf, i))) + .put(KEY_BASAL, JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedBasal, i))) + .put(KEY_TARGET_LOW, JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetLow, i))) + .put(KEY_TARGET_HIGH, JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetHigh, i))) profilesList.add( SingleProfile( name = name, mgdl = preferences.get(ProfileComposedBooleanKey.LocalProfileNumberedMgdl, i), - ic = JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIc, i)), - isf = JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIsf, i)), - basal = JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedBasal, i)), - targetLow = JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetLow, i)), - targetHigh = JSONArray(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetHigh, i)) + ic = readSchedule(entry, KEY_IC, name), + isf = readSchedule(entry, KEY_ISF, name), + basal = readSchedule(entry, KEY_BASAL, name), + target = readTargetSchedule(entry, name) ) ) } catch (e: JSONException) { @@ -465,7 +468,13 @@ class ProfileRepositoryImpl @Inject constructor( aapsLogger.debug(LTag.PROFILE, "Storing settings ($origin): " + rawProfile?.getData().toString()) } - /** The stored form: one document carrying its own edit stamp plus the profile list. */ + /** + * The stored form: one document carrying its own edit stamp plus the profile list. + * + * The field names and the per-schedule array shape are the master↔client sync wire format and + * the 3.4.x upgrade path, so they are rendered back exactly as before — the in-memory model + * changed, the document did not. Targets are one paired list in memory and two arrays here. + */ private fun profilesToJson(timestamp: Long): JSONObject { val array = JSONArray() for (profile in profilesList) { @@ -473,11 +482,11 @@ class ProfileRepositoryImpl @Inject constructor( JSONObject() .put(KEY_NAME, profile.name) .put(KEY_MGDL, profile.mgdl) - .put(KEY_IC, profile.ic) - .put(KEY_ISF, profile.isf) - .put(KEY_BASAL, profile.basal) - .put(KEY_TARGET_LOW, profile.targetLow) - .put(KEY_TARGET_HIGH, profile.targetHigh) + .put(KEY_IC, profile.ic.toJSONArray()) + .put(KEY_ISF, profile.isf.toJSONArray()) + .put(KEY_BASAL, profile.basal.toJSONArray()) + .put(KEY_TARGET_LOW, profile.target.lowToJSONArray()) + .put(KEY_TARGET_HIGH, profile.target.highToJSONArray()) ) } return JSONObject().put(KEY_LAST_CHANGE, timestamp).put(KEY_PROFILES, array) @@ -508,11 +517,10 @@ class ProfileRepositoryImpl @Inject constructor( // current units: this runs from init(), and resolving that Lazy there closes a // dependency cycle back into this repository (crash at startup). mgdl = entry.optBoolean(KEY_MGDL, ProfileComposedBooleanKey.LocalProfileNumberedMgdl.defaultValue), - ic = entry.optJSONArray(KEY_IC) ?: emptyBlockArray(), - isf = entry.optJSONArray(KEY_ISF) ?: emptyBlockArray(), - basal = entry.optJSONArray(KEY_BASAL) ?: emptyBlockArray(), - targetLow = entry.optJSONArray(KEY_TARGET_LOW) ?: emptyBlockArray(), - targetHigh = entry.optJSONArray(KEY_TARGET_HIGH) ?: emptyBlockArray() + ic = readSchedule(entry, KEY_IC, name), + isf = readSchedule(entry, KEY_ISF, name), + basal = readSchedule(entry, KEY_BASAL, name), + target = readTargetSchedule(entry, name) ) ) } @@ -523,10 +531,31 @@ class ProfileRepositoryImpl @Inject constructor( } } - /** The same placeholder the legacy per-profile keys used, so a missing block reads as "unset", not as valid. */ - private fun emptyBlockArray(): JSONArray = singleBlockProfileArray(0.0) + /** + * One schedule out of a stored profile entry, or the zero placeholder when it cannot be read. + * + * "Cannot be read" now covers a damaged array as well as a missing one — with typed blocks the + * parse either succeeds or it does not, where the old code could carry unreadable JSON around + * untouched. The outcome is the documented one either way: a zero schedule is out of hard limits, + * so the profile shows as invalid in the editor and is refused by save and by sync. It fails + * where the user can see and fix it, instead of reaching the pump. + */ + private fun readSchedule(entry: JSONObject, key: String, profileName: String): List = + blockFromJsonArray(entry.optJSONArray(key), dateUtil) ?: run { + if (entry.has(key)) aapsLogger.error(LTag.PROFILE, "Cannot read '$key' of profile '$profileName', using zero") + singleBlock(0.0) + } + + /** [readSchedule] for the paired target arrays. */ + private fun readTargetSchedule(entry: JSONObject, profileName: String): List = + targetBlockFromJsonArray(entry.optJSONArray(KEY_TARGET_LOW), entry.optJSONArray(KEY_TARGET_HIGH), dateUtil) ?: run { + if (entry.has(KEY_TARGET_LOW) || entry.has(KEY_TARGET_HIGH)) + aapsLogger.error(LTag.PROFILE, "Cannot read targets of profile '$profileName', using zero") + singleTargetBlock(0.0, 0.0) + } - private fun loadFromStoreInternal(store: ProfileStore) { + /** @return true when the incoming store replaced the list, false when it was rejected. */ + private fun loadFromStoreInternal(store: ProfileStore): Boolean { try { val newProfiles: ArrayList = ArrayList() for (p in store.getProfileList()) { @@ -539,11 +568,9 @@ class ProfileRepositoryImpl @Inject constructor( if (pureProfile != null && validityCheck.isValid) { // copyFrom would timestamp-suffix the name if it collides with the // CURRENT store, but here we're REPLACING the whole list — the NS name - // should be preserved verbatim. Reuse copyFrom for the JSON-field - // unpacking, then restore the raw name. - val sp = copyFrom(pureProfile, p.toString()) - sp.name = p.toString() - newProfiles.add(sp) + // should be preserved verbatim. Reuse copyFrom for the field copying, + // then restore the raw name. + newProfiles.add(copyFrom(pureProfile, p.toString()).copy(name = p.toString())) } else { notificationManager.post( NotificationId.INVALID_PROFILE_NOT_ACCEPTED, @@ -556,12 +583,13 @@ class ProfileRepositoryImpl @Inject constructor( aapsLogger.debug(LTag.PROFILE, "Accepted ${profilesList.size} profiles") // Adopted, not authored: a Nightscout store must not be pushed back to the master. storeSettingsInternal(timestamp = store.getStartDate(), origin = WriteOrigin.ADOPTED) - } else { - aapsLogger.debug(LTag.PROFILE, "ProfileStore not accepted") + return true } + aapsLogger.debug(LTag.PROFILE, "ProfileStore not accepted") } catch (e: Exception) { aapsLogger.error("Error loading ProfileStore", e) } + return false } private fun addNewProfileInternal() { @@ -576,24 +604,14 @@ class ProfileRepositoryImpl @Inject constructor( SingleProfile( name = Constants.LOCAL_PROFILE + free, mgdl = isMgdl, - ic = singleBlockProfileArray(15.0), - isf = singleBlockProfileArray(if (isMgdl) 100.0 else 5.6), - basal = singleBlockProfileArray(0.1), - targetLow = singleBlockProfileArray(if (isMgdl) 110.0 else 6.1), - targetHigh = singleBlockProfileArray(if (isMgdl) 120.0 else 6.7) + ic = singleBlock(15.0), + isf = singleBlock(if (isMgdl) 100.0 else 5.6), + basal = singleBlock(0.1), + target = singleTargetBlock(if (isMgdl) 110.0 else 6.1, if (isMgdl) 120.0 else 6.7) ) ) } - /** A single 00:00 profile block carrying [value], matching the JSON shape AAPS profiles expect. */ - private fun singleBlockProfileArray(value: Double): JSONArray = - JSONArray().put( - JSONObject() - .put("time", "00:00") - .put("timeAsSeconds", 0) - .put("value", value) - ) - private fun createAndStoreConvertedProfile() { val json = JSONObject() val store = JSONObject() @@ -601,11 +619,11 @@ class ProfileRepositoryImpl @Inject constructor( for (i in profilesList.indices) { profilesList[i].run { val pj = JSONObject() - pj.put("carbratio", ic) - pj.put("sens", isf) - pj.put("basal", basal) - pj.put("target_low", targetLow) - pj.put("target_high", targetHigh) + pj.put("carbratio", ic.toJSONArray()) + pj.put("sens", isf.toJSONArray()) + pj.put("basal", basal.toJSONArray()) + pj.put("target_low", target.lowToJSONArray()) + pj.put("target_high", target.highToJSONArray()) pj.put("units", if (mgdl) GlucoseUnit.MGDL.asText else GlucoseUnit.MMOL.asText) pj.put("timezone", TimeZone.getDefault().id) store.put(name, pj) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt index 040512b94cfe..e7dcfefe4e2f 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt @@ -1,11 +1,15 @@ package app.aaps.implementation.profile +import app.aaps.core.interfaces.profile.ProfileStore import app.aaps.core.interfaces.profile.SingleProfile import app.aaps.core.keys.LongNonKey import app.aaps.core.keys.ProfileComposedBooleanKey import app.aaps.core.keys.ProfileComposedStringKey import app.aaps.core.keys.ProfileIntKey import app.aaps.core.keys.StringNonKey +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock +import app.aaps.core.objects.extensions.toJSONArray import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat import dagger.Lazy @@ -23,6 +27,7 @@ import org.mockito.kotlin.atLeast import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions @@ -34,17 +39,13 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) class ProfileRepositoryImplTest : TestBaseWithProfile() { - private fun singleBlock(value: Double): JSONArray = - JSONArray().put(JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", value)) - private fun profile(name: String) = SingleProfile( name = name, mgdl = true, ic = singleBlock(15.0), isf = singleBlock(100.0), basal = singleBlock(0.1), - targetLow = singleBlock(110.0), - targetHigh = singleBlock(120.0) + target = singleTargetBlock(110.0, 120.0) ) // The document the repository reads on start, and the flow it watches for values arriving from @@ -72,11 +73,11 @@ class ProfileRepositoryImplTest : TestBaseWithProfile() { JSONObject() .put("name", name) .put("mgdl", true) - .put("ic", singleBlock(15.0)) - .put("isf", singleBlock(100.0)) - .put("basal", singleBlock(0.1)) - .put("targetLow", singleBlock(110.0)) - .put("targetHigh", singleBlock(120.0)) + .put("ic", singleBlock(15.0).toJSONArray()) + .put("isf", singleBlock(100.0).toJSONArray()) + .put("basal", singleBlock(0.1).toJSONArray()) + .put("targetLow", singleBlock(110.0).toJSONArray()) + .put("targetHigh", singleBlock(120.0).toJSONArray()) ) } }) @@ -190,11 +191,11 @@ class ProfileRepositoryImplTest : TestBaseWithProfile() { names.forEachIndexed { i, name -> whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedName, i)).thenReturn(name) whenever(preferences.get(ProfileComposedBooleanKey.LocalProfileNumberedMgdl, i)).thenReturn(true) - whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIc, i)).thenReturn(singleBlock(15.0).toString()) - whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIsf, i)).thenReturn(singleBlock(100.0).toString()) - whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedBasal, i)).thenReturn(singleBlock(0.1).toString()) - whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetLow, i)).thenReturn(singleBlock(110.0).toString()) - whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetHigh, i)).thenReturn(singleBlock(120.0).toString()) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIc, i)).thenReturn(singleBlock(15.0).toJSONArray().toString()) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIsf, i)).thenReturn(singleBlock(100.0).toJSONArray().toString()) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedBasal, i)).thenReturn(singleBlock(0.1).toJSONArray().toString()) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetLow, i)).thenReturn(singleBlock(110.0).toJSONArray().toString()) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetHigh, i)).thenReturn(singleBlock(120.0).toJSONArray().toString()) } } @@ -339,4 +340,37 @@ class ProfileRepositoryImplTest : TestBaseWithProfile() { assertThat(adoptedWrites()).isNotEmpty() assertThat(localWrites()).isEmpty() } + + /** + * A store Nightscout pushed but we refused changes nothing, so it must not count as a mutation. + * + * [ProfileRepositoryImpl.revision] means "something happened", and the profile editor reloads its + * working copy on every bump — so bumping here would throw away edits the user was in the middle + * of typing, for an event that did not touch a single profile. The old code got this right by + * accident: profiles were compared by identity, so re-publishing the same list simply did not emit. + */ + @Test + fun `a rejected Nightscout store does not count as a mutation`() = runTest { + val sut = createSut() + sut.add(profile("Mine")) + val revisionBefore = sut.revision.value + val listBefore = sut.profiles.value + + // An empty store has no profile to accept, so loadFromStoreInternal rejects it. + sut.loadFromNs(mock().also { whenever(it.getProfileList()).thenReturn(ArrayList()) }) + + assertThat(sut.revision.value).isEqualTo(revisionBefore) + assertThat(sut.profiles.value).isSameInstanceAs(listBefore) + } + + /** The accepted case still bumps, otherwise the editor would never notice an NS push. */ + @Test + fun `an accepted Nightscout store does count as a mutation`() = runTest { + val sut = createSut() + val revisionBefore = sut.revision.value + + sut.loadFromNs(getValidProfileStore()) + + assertThat(sut.revision.value).isGreaterThan(revisionBefore) + } } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index b0d48341e5b0..c9b87f5df24d 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -31,6 +31,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.IntKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.objects.extensions.blockFromJsonArray import app.aaps.core.objects.extensions.pureProfileFromJson import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.compose.icons.IcPluginAutotune @@ -407,13 +408,15 @@ class AutotunePlugin @Inject constructor( profileRepository.add(profileRepository.copyFrom(newProfile.getProfile(circadian), newProfile.profileName)) return } - // Snapshot-and-commit: deep-clone the existing profile, mutate the clone, hand it - // back to the repo. Avoids in-place mutation of the shared list element. - val updated = profileRepository.profiles.value[indexLocalProfile].deepClone().apply { - basal = newProfile.basal() - ic = newProfile.ic(circadian) - isf = newProfile.isf(circadian) - } + // Copy-and-commit: the stored profile is immutable, so build the tuned version with copy() + // and hand that back to the repo. A schedule that cannot be read is left at its old value + // rather than replaced with something unusable. + val existing = profileRepository.profiles.value[indexLocalProfile] + val updated = existing.copy( + basal = blockFromJsonArray(newProfile.basal(), dateUtil) ?: existing.basal, + ic = blockFromJsonArray(newProfile.ic(circadian), dateUtil) ?: existing.ic, + isf = blockFromJsonArray(newProfile.isf(circadian), dateUtil) ?: existing.isf + ) profileRepository.replace(indexLocalProfile, updated) } diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt index f21474f9a62f..c4aa11083354 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt @@ -112,7 +112,6 @@ class AutotuneCoreTest : TestBaseWithProfile() { } val pure = PureProfile( - jsonObject = jsonObject, basalBlocks = basalBlocks, isfBlocks = isfBlocks, icBlocks = icBlocks, diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt index 827197a296ea..9c29daf33f9f 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt @@ -183,7 +183,6 @@ class AutotunePrepTest : TestBaseWithProfile() { } val pure = PureProfile( - jsonObject = jsonObject, basalBlocks = basalBlocks.shiftBlock(1.0, ts), isfBlocks = isfBlocks, icBlocks = icBlocks, diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt index 75f8feed7274..c0f01f70c873 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt @@ -159,6 +159,7 @@ open class TestBaseWithProfile : TestBase() { whenever(preferences.observe(any())).thenReturn(MutableStateFlow(0)) whenever(preferences.observe(any())).thenReturn(MutableStateFlow(0L)) whenever(profileRepository.profiles).thenReturn(MutableStateFlow(emptyList())) + whenever(profileRepository.revision).thenReturn(MutableStateFlow(0L)) whenever(profileRepository.profile).thenReturn(MutableStateFlow(getValidProfileStore())) deltaCalculator = DeltaCalculator(aapsLogger) apsResultProvider = Provider { DetermineBasalResult(aapsLogger, fabricPrivacy, constraintsChecker, preferences, activePlugin, processedTbrEbData, profileFunction, rh, decimalFormatter, dateUtil, apsResultProvider, ch) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModel.kt index 30a475315b24..477277026384 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModel.kt @@ -5,6 +5,9 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.data.model.data.Block +import app.aaps.core.data.model.data.TargetBlock +import app.aaps.core.data.time.T import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.plugin.ActivePlugin @@ -29,10 +32,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.json.JSONArray -import org.json.JSONObject import java.math.RoundingMode -import java.util.Locale import javax.inject.Inject data class TimeValue( @@ -106,23 +106,24 @@ class ProfileEditorViewModel @Inject constructor( private val _uiState = MutableStateFlow(ProfileUiState()) val uiState: StateFlow = _uiState.asStateFlow() - // VM-local edit state. The profile here is a deep clone of the repo's version at - // selection time; edits mutate this clone and commit via [profileRepository.replace]. + // VM-local edit state. [SingleProfile] is immutable, so this is simply the repo's version at + // selection time with the user's edits applied on top via copy(); it commits through + // [profileRepository.replace]. Nothing is shared with the repository, so no clone is needed. private var editingIndex: Int = 0 private var editingProfile: SingleProfile? = null private var locallyEdited: Boolean = false // True while editing a not-yet-persisted "new profile" draft. In this mode [editingIndex] is a // sentinel (-1): the profile has no row in the store, so [saveProfile] appends via the repo's - // add() and the external-change subscriber must never re-clone from the (nonexistent) index. + // add() and the external-change subscriber must never reload from the (nonexistent) index. private var isNewDraft: Boolean = false - // Content of the last profile this editor saved, used to recognise the echo of our own save in - // the [profileRepository.profiles] stream. It is NOT a one-shot flag on purpose: one save can - // produce two emits — the local write, and then (on a paired client) the master's authoritative - // copy coming back through the sync channel a moment later. A flag would be consumed by the first - // and let the second wipe edits the user typed meanwhile; comparing content survives both. - private var lastSavedContent: String? = null + // The last profile this editor saved, used to recognise the echo of our own save. It is NOT a + // one-shot flag on purpose: one save can produce two emits — the local write, and then (on a + // paired client) the master's authoritative copy coming back through the sync channel a moment + // later. A flag would be consumed by the first and let the second wipe edits the user typed + // meanwhile; comparing content survives both. + private var lastSaved: SingleProfile? = null init { viewModelScope.launch { loadState() } @@ -130,28 +131,31 @@ class ProfileEditorViewModel @Inject constructor( } private fun subscribeToEvents() { - // Drop the StateFlow's replayed initial value — we only care about subsequent changes. - profileRepository.profiles.drop(1) + // Watch [revision], not [profiles]. Profiles are compared structurally now, so a mutation + // that lands on an identical list does not emit there — and "reset while the stored profile + // happens to match what is on screen" is exactly such a case. Reset must still reload. + // Drop the StateFlow's replayed initial value; we only care about subsequent changes. + profileRepository.revision.drop(1) .onEach { - aapsLogger.debug(LTag.PROFILE, "profileRepository.profiles changed") + aapsLogger.debug(LTag.PROFILE, "profileRepository changed") if (isNewDraft) { // Uncommitted draft — its index doesn't exist in the persisted list. Never - // re-clone (that would wipe the draft); just refresh derived UI state. + // reload (that would wipe the draft); just refresh derived UI state. loadState() return@onEach } val stored = profileRepository.profiles.value.getOrNull(editingIndex) - if (lastSavedContent != null && stored?.contentKey() == lastSavedContent) { + if (lastSaved != null && stored == lastSaved) { // The emit carries exactly what we saved — our own write, or the master echoing - // it back after applying it. The live `editingProfile` reference is correct as-is - // (and may already hold newer typing). Skip the re-clone, just refresh the UI. + // it back after applying it. The working copy is correct as-is (and may already + // hold newer typing). Skip the reload, just refresh the UI. loadState() } else { // Somebody else changed this profile (NS push, master edit, editor reset). - // Reload the clone — discards in-flight edits. - editingProfile = stored?.deepClone() + // Reload — discards in-flight edits. + editingProfile = stored locallyEdited = false - lastSavedContent = null + lastSaved = null loadState() } }.launchIn(viewModelScope) @@ -235,7 +239,7 @@ class ProfileEditorViewModel @Inject constructor( fun selectProfile(index: Int) { isNewDraft = false editingIndex = index - editingProfile = profileRepository.profiles.value.getOrNull(index)?.deepClone() + editingProfile = profileRepository.profiles.value.getOrNull(index) locallyEdited = false viewModelScope.launch { loadState() } } @@ -253,167 +257,135 @@ class ProfileEditorViewModel @Inject constructor( viewModelScope.launch { loadState() } } - fun updateProfileName(name: String) { - editingProfile?.name = name - markEdited() - } + fun updateProfileName(name: String) = editProfile { it.copy(name = name) } - fun updateIcEntry(index: Int, timeValue: TimeValue) { - editingProfile?.let { profile -> - updateJsonArrayEntry(profile.ic, index, timeValue) - markEdited() - } - } + fun updateIcEntry(index: Int, timeValue: TimeValue) = + editProfile { it.copy(ic = it.ic.timeValues().updateAt(index, timeValue).toBlocks()) } - fun updateIsfEntry(index: Int, timeValue: TimeValue) { - editingProfile?.let { profile -> - updateJsonArrayEntry(profile.isf, index, timeValue) - markEdited() - } - } + fun updateIsfEntry(index: Int, timeValue: TimeValue) = + editProfile { it.copy(isf = it.isf.timeValues().updateAt(index, timeValue).toBlocks()) } - fun updateBasalEntry(index: Int, timeValue: TimeValue) { - editingProfile?.let { profile -> - updateJsonArrayEntry(profile.basal, index, timeValue) - markEdited() - } - } + fun updateBasalEntry(index: Int, timeValue: TimeValue) = + editProfile { it.copy(basal = it.basal.timeValues().updateAt(index, timeValue).toBlocks()) } - fun updateTargetEntry(index: Int, low: TimeValue, high: TimeValue) { - editingProfile?.let { profile -> - updateJsonArrayEntry(profile.targetLow, index, low) - updateJsonArrayEntry(profile.targetHigh, index, high) - markEdited() + fun updateTargetEntry(index: Int, low: TimeValue, high: TimeValue) = + editProfile { + it.copy( + target = toTargetBlocks( + it.target.lowTimeValues().updateAt(index, low), + it.target.highTimeValues().updateAt(index, high) + ) + ) } - } - fun addIcEntry(afterIndex: Int) { - editingProfile?.let { profile -> - addJsonArrayEntry(profile.ic, afterIndex) - markEdited() - } - } + fun addIcEntry(afterIndex: Int) = editProfile { it.copy(ic = it.ic.timeValues().addAfter(afterIndex).toBlocks()) } - fun addIsfEntry(afterIndex: Int) { - editingProfile?.let { profile -> - addJsonArrayEntry(profile.isf, afterIndex) - markEdited() - } - } + fun addIsfEntry(afterIndex: Int) = editProfile { it.copy(isf = it.isf.timeValues().addAfter(afterIndex).toBlocks()) } - fun addBasalEntry(afterIndex: Int) { - editingProfile?.let { profile -> - addJsonArrayEntry(profile.basal, afterIndex) - markEdited() - } - } + fun addBasalEntry(afterIndex: Int) = editProfile { it.copy(basal = it.basal.timeValues().addAfter(afterIndex).toBlocks()) } - fun addTargetEntry(afterIndex: Int) { - editingProfile?.let { profile -> - addJsonArrayEntry(profile.targetLow, afterIndex) - addJsonArrayEntry(profile.targetHigh, afterIndex) - markEdited() + fun addTargetEntry(afterIndex: Int) = + editProfile { + it.copy( + target = toTargetBlocks( + it.target.lowTimeValues().addAfter(afterIndex), + it.target.highTimeValues().addAfter(afterIndex) + ) + ) } - } - fun removeIcEntry(index: Int) { - editingProfile?.let { profile -> - if (profile.ic.length() > 1 && index > 0) { - profile.ic.remove(index) - markEdited() - } - } - } + fun removeIcEntry(index: Int) = editProfile { it.copy(ic = it.ic.timeValues().removeAt(index).toBlocks()) } - fun removeIsfEntry(index: Int) { - editingProfile?.let { profile -> - if (profile.isf.length() > 1 && index > 0) { - profile.isf.remove(index) - markEdited() - } - } - } + fun removeIsfEntry(index: Int) = editProfile { it.copy(isf = it.isf.timeValues().removeAt(index).toBlocks()) } - fun removeBasalEntry(index: Int) { - editingProfile?.let { profile -> - if (profile.basal.length() > 1 && index > 0) { - profile.basal.remove(index) - markEdited() - } - } - } + fun removeBasalEntry(index: Int) = editProfile { it.copy(basal = it.basal.timeValues().removeAt(index).toBlocks()) } - fun removeTargetEntry(index: Int) { - editingProfile?.let { profile -> - if (profile.targetLow.length() > 1 && index > 0) { - profile.targetLow.remove(index) - profile.targetHigh.remove(index) - markEdited() - } + fun removeTargetEntry(index: Int) = + editProfile { + it.copy( + target = toTargetBlocks( + it.target.lowTimeValues().removeAt(index), + it.target.highTimeValues().removeAt(index) + ) + ) } - } - private fun updateJsonArrayEntry(array: JSONArray, index: Int, timeValue: TimeValue) { - if (index < array.length()) { - val obj = array.getJSONObject(index) - val hour = timeValue.timeSeconds / 3600 - obj.put("time", String.format(Locale.getDefault(), "%02d:00", hour)) - obj.put("timeAsSeconds", timeValue.timeSeconds) - obj.put("value", timeValue.value) - } + /** + * Apply [transform] to the working copy and refresh the UI. + * + * An edit that changes nothing is dropped, so re-picking the value that is already there no + * longer lights up "unsaved changes". That check needs structural equality, which arrived with + * [SingleProfile] becoming immutable data; the previous mutate-in-place code could not tell the + * difference and always marked the profile edited. + */ + private fun editProfile(transform: (SingleProfile) -> SingleProfile) { + val current = editingProfile ?: return + val updated = transform(current) + if (updated == current) return + editingProfile = updated + markEdited() } - private fun addJsonArrayEntry(array: JSONArray, afterIndex: Int) { - if (array.length() >= 24) return + // ----------------------------------------------------------------------------------------- + // Blocks <-> rows. A [Block] carries a DURATION, while the editor shows a START TIME per row, + // so a row's duration depends on the row after it. These two conversions are the only place + // that relationship is expressed. + // ----------------------------------------------------------------------------------------- - val prevObj = if (afterIndex >= 0 && afterIndex < array.length()) { - array.getJSONObject(afterIndex) - } else { - null - } + private fun List.timeValues(): List { + var start = 0 + return map { block -> TimeValue(start, block.amount).also { start += T.msecs(block.duration).secs().toInt() } } + } - val newTime = if (prevObj != null) { - prevObj.getInt("timeAsSeconds") + 3600 // Add 1 hour after current entry - } else { - 0 - } + private fun List.lowTimeValues(): List = timeValues { it.lowTarget } - if (newTime >= 24 * 3600) return + private fun List.highTimeValues(): List = timeValues { it.highTarget } - // Copy value from previous entry (the one before it) - val inheritedValue = prevObj?.optDouble("value", 0.0) ?: 0.0 + private fun List.timeValues(select: (TargetBlock) -> Double): List { + var start = 0 + return map { block -> TimeValue(start, select(block)).also { start += T.msecs(block.duration).secs().toInt() } } + } - val newObj = JSONObject().apply { - val hour = newTime / 3600 - put("time", String.format(Locale.getDefault(), "%02d:00", hour)) - put("timeAsSeconds", newTime) - put("value", inheritedValue) - } + private fun List.toBlocks(): List = + mapIndexed { index, row -> Block(durationMs(index, row), row.value) } - // Insert at position afterIndex + 1 - val insertPos = afterIndex + 1 - val tempList = mutableListOf() - for (i in 0 until array.length()) { - tempList.add(array.getJSONObject(i)) + /** Low and high always share their times, so the low side drives the durations. */ + private fun toTargetBlocks(low: List, high: List): List = + low.mapIndexed { index, row -> + TargetBlock(low.durationMs(index, row), row.value, high.getOrNull(index)?.value ?: row.value) } - tempList.add(insertPos.coerceIn(0, tempList.size), newObj) - // Clear and rebuild array - while (array.length() > 0) array.remove(0) - tempList.forEach { array.put(it) } + /** A row lasts until the next row starts; the last one runs to midnight. */ + private fun List.durationMs(index: Int, row: TimeValue): Long = + ((getOrNull(index + 1)?.timeSeconds ?: DAY_SECONDS) - row.timeSeconds) * 1000L + + // ----------------------------------------------------------------------------------------- + // Row operations. Each returns a new list and keeps the previous JSON-based limits: at most 24 + // rows, a new row starts one hour after the one it follows, and row 0 can never be removed + // because the schedule has to start at midnight. + // ----------------------------------------------------------------------------------------- + + private fun List.updateAt(index: Int, row: TimeValue): List = + if (index in indices) toMutableList().also { it[index] = row } else this + + private fun List.addAfter(afterIndex: Int): List { + if (size >= 24) return this + val previous = getOrNull(afterIndex) + val newTime = previous?.let { it.timeSeconds + 3600 } ?: 0 + if (newTime >= DAY_SECONDS) return this + // The new row inherits the value of the row it follows, so adding a row alone never changes + // what the profile delivers. + return toMutableList().also { it.add((afterIndex + 1).coerceIn(0, it.size), TimeValue(newTime, previous?.value ?: 0.0)) } } + private fun List.removeAt(index: Int): List = + if (size > 1 && index > 0) toMutableList().also { it.removeAt(index) } else this + private fun markEdited() { locallyEdited = true viewModelScope.launch { loadState() } } - /** - * Stable content fingerprint of a profile. [SingleProfile] holds `JSONArray` fields, which have no - * structural `equals`, so the serialised form is what can be compared. - */ - private fun SingleProfile.contentKey(): String = "$name|$mgdl|$ic|$isf|$basal|$targetLow|$targetHigh" - fun saveProfile() { viewModelScope.launch { val profile = editingProfile ?: return@launch @@ -425,11 +397,11 @@ class ProfileEditorViewModel @Inject constructor( return@launch } // Remember what we are about to persist so the resulting emit(s) are recognised as ours. - lastSavedContent = profile.contentKey() + lastSaved = profile if (isNewDraft) { - // Commit the draft as a new profile. deepClone so the stored copy is independent of - // the editor's working reference (mirrors replace()'s defensive clone). - profileRepository.add(profile.deepClone()) + // Commit the draft as a new profile. No clone needed - SingleProfile is immutable, so + // the editor keeping its own reference cannot reach into the store. + profileRepository.add(profile) .onSuccess { // Draft is now persisted: leave draft mode and point at its row so further // saves go through replace(). @@ -457,7 +429,7 @@ class ProfileEditorViewModel @Inject constructor( // Forget the expected echo so the NEXT external event isn't mis-attributed to // this failed save. Surface the error in the log; the user keeps their unsaved // edits visible in the editor (locallyEdited stays true). - lastSavedContent = null + lastSaved = null aapsLogger.error(LTag.PROFILE, "saveProfile failed at index $editingIndex", error) } } @@ -479,29 +451,19 @@ class ProfileEditorViewModel @Inject constructor( viewModelScope.launch { profileRepository.reset() } } - private fun SingleProfile.toState(): SingleProfileState { - return SingleProfileState( + private fun SingleProfile.toState(): SingleProfileState = + SingleProfileState( name = name, mgdl = mgdl, - ic = ic.toTimeValueList(), - isf = isf.toTimeValueList(), - basal = basal.toTimeValueList(), - targetLow = targetLow.toTimeValueList(), - targetHigh = targetHigh.toTimeValueList() + ic = ic.timeValues(), + isf = isf.timeValues(), + basal = basal.timeValues(), + targetLow = target.lowTimeValues(), + targetHigh = target.highTimeValues() ) - } - private fun JSONArray.toTimeValueList(): List { - val list = mutableListOf() - for (i in 0 until length()) { - val obj = getJSONObject(i) - list.add( - TimeValue( - timeSeconds = obj.optInt("timeAsSeconds", 0), - value = obj.optDouble("value", 0.0) - ) - ) - } - return list + private companion object { + + const val DAY_SECONDS = 24 * 3600 } } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModelTest.kt index 613108456af6..f9de68b68601 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileEditorViewModelTest.kt @@ -5,6 +5,8 @@ import app.aaps.core.interfaces.profile.ProfileErrorType import app.aaps.core.interfaces.profile.ProfileValidationError import app.aaps.core.interfaces.profile.SingleProfile import app.aaps.core.interfaces.protection.ProtectionCheck +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -14,8 +16,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.json.JSONArray -import org.json.JSONObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -37,11 +37,9 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { @Mock lateinit var protectionCheck: ProtectionCheck private val profilesFlow = MutableStateFlow>(emptyList()) + private val revisionFlow = MutableStateFlow(0L) private lateinit var sut: ProfileEditorViewModel - private fun singleBlock(value: Double): JSONArray = - JSONArray().put(JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", value)) - // A well-formed, parseable profile (validity is driven by the validateStructured stub, not content). private fun profile(name: String) = SingleProfile( name = name, @@ -49,14 +47,24 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { ic = singleBlock(15.0), isf = singleBlock(100.0), basal = singleBlock(0.1), - targetLow = singleBlock(110.0), - targetHigh = singleBlock(120.0) + target = singleTargetBlock(110.0, 120.0) ) + /** + * Publish a profile list the way the repository does: list first, then the revision bump. The + * editor watches the revision, so a test that only set the list would be publishing a change the + * editor never hears about — and an identical list would not even emit. + */ + private fun publish(vararg profiles: SingleProfile) { + profilesFlow.value = profiles.toList() + revisionFlow.value = revisionFlow.value + 1 + } + @BeforeEach fun setUp() { Dispatchers.setMain(UnconfinedTestDispatcher()) whenever(profileRepository.profiles).thenReturn(profilesFlow) + whenever(profileRepository.revision).thenReturn(revisionFlow) whenever(profileRepository.newDraft()).thenReturn(profile("LocalProfile1")) whenever(profileFunction.getUnits()).thenReturn(GlucoseUnit.MGDL) whenever(protectionCheck.isLocked(any())).thenReturn(false) @@ -96,7 +104,7 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { @Test fun existingProfileSavedAsReplaceNotAdd() = runTest { whenever(profileRepository.replace(any(), any())).thenReturn(Result.success(Unit)) - profilesFlow.value = listOf(profile("Existing")) + publish(profile("Existing")) sut.selectProfile(0) sut.saveProfile() @@ -112,9 +120,9 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { val draftName = sut.uiState.value.currentProfile?.name // An external profile-list change (e.g. an NS push) arrives while the draft is open. - profilesFlow.value = listOf(profile("PushedFromNs")) + publish(profile("PushedFromNs")) - // The draft survives (its index doesn't exist in the list, so it must not be re-cloned away)... + // The draft survives (its index doesn't exist in the list, so it must not be reloaded away)... assertThat(sut.uiState.value.currentProfile?.name).isEqualTo(draftName) // ...and still commits as a new profile. sut.saveProfile() @@ -128,17 +136,17 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { // change may re-clone the editor. whenever(profileRepository.replace(any(), any())).thenReturn(Result.success(Unit)) val saved = profile("Existing") - profilesFlow.value = listOf(saved) + publish(saved) sut.selectProfile(0) sut.saveProfile() - // First emit: the local write. Fresh instances each time — SingleProfile has no structural - // equals, so distinct objects are what make the StateFlow emit at all. - profilesFlow.value = listOf(saved.deepClone()) + // First emit: the local write. The content is identical to what we saved, which is exactly + // why this rides on the revision counter — the profile list itself does not emit here. + publish(saved) // The user keeps typing while the round-trip is in flight. sut.updateProfileName("RenamedWhileInFlight") // Second emit: the same content echoed back after the master applied it. - profilesFlow.value = listOf(saved.deepClone()) + publish(saved) assertThat(sut.uiState.value.currentProfile?.name).isEqualTo("RenamedWhileInFlight") } @@ -146,13 +154,50 @@ internal class ProfileEditorViewModelTest : TestBaseWithProfile() { @Test fun aForeignProfileChangeStillReloadsTheEditor() = runTest { whenever(profileRepository.replace(any(), any())).thenReturn(Result.success(Unit)) - profilesFlow.value = listOf(profile("Existing")) + publish(profile("Existing")) sut.selectProfile(0) sut.saveProfile() // Different content at our index — somebody else edited this profile. - profilesFlow.value = listOf(profile("ChangedByMaster")) + publish(profile("ChangedByMaster")) assertThat(sut.uiState.value.currentProfile?.name).isEqualTo("ChangedByMaster") } + + /** + * The reason the editor watches [app.aaps.core.interfaces.profile.ProfileRepository.revision] + * rather than the profile list. + * + * Reset re-reads the stored profile and republishes it. When the user's edit happened to bring + * the profile back to what is already stored, the republished list is structurally equal to the + * previous one, so the list StateFlow does not emit at all. Watching the list would leave the + * editor showing the edit it was asked to discard, with Save still offered. + */ + @Test + fun resetToAnIdenticalListStillDiscardsTheEdit() = runTest { + val stored = profile("Existing") + publish(stored) + sut.selectProfile(0) + + sut.updateProfileName("TypedButNotSaved") + assertThat(sut.uiState.value.isEdited).isTrue() + + // reset() reloads from storage and publishes the same list it already had. + publish(stored) + + assertThat(sut.uiState.value.currentProfile?.name).isEqualTo("Existing") + assertThat(sut.uiState.value.isEdited).isFalse() + } + + /** A no-op edit is not an edit: re-entering the value already on screen must not offer Save. */ + @Test + fun reEnteringTheSameValueDoesNotMarkTheProfileEdited() = runTest { + publish(profile("Existing")) + sut.selectProfile(0) + + sut.updateProfileName("Existing") + sut.updateBasalEntry(0, TimeValue(0, 0.1)) + + assertThat(sut.uiState.value.isEdited).isFalse() + } } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt index 3e1aab8675fe..e0aa426e1708 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt @@ -19,6 +19,8 @@ import app.aaps.core.interfaces.sync.NsClient import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.keys.interfaces.Preferences +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.ui.compose.ScreenMode import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -31,8 +33,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.json.JSONArray -import org.json.JSONObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -68,17 +68,13 @@ internal class ProfileManagementViewModelTest { private val profilesFlow = MutableStateFlow>(emptyList()) - private fun singleBlock(value: Double): JSONArray = - JSONArray().put(JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", value)) - private fun profile(name: String) = SingleProfile( name = name, mgdl = true, ic = singleBlock(15.0), isf = singleBlock(100.0), basal = singleBlock(0.1), - targetLow = singleBlock(110.0), - targetHigh = singleBlock(120.0) + target = singleTargetBlock(110.0, 120.0) ) /** Publish [count] profiles named P0..P(count-1) as the repository's current list. */ From 37a9cedf5d12a836bf403789d968098e07dc24e8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 17:06:52 +0200 Subject: [PATCH 065/146] Replace Spanned with AnnotatedString, drop the unused APS result renderers --- .../aaps/e2e/AbstractDanaEmulatorUiTest.kt | 21 +++---- .../app/aaps/e2e/DanaRPairWizardUiTest.kt | 20 +++---- .../app/aaps/e2e/DanaRSPairWizardUiTest.kt | 20 +++---- .../aaps/e2e/EquilActivationWizardUiTest.kt | 20 +++---- .../aaps/e2e/EquilEmulatorActivationTest.kt | 21 +++---- .../plugins/aps/openAPS/APSResultObject.kt | 31 ----------- .../app/aaps/core/interfaces/aps/APSResult.kt | 3 - .../core/interfaces/queue/CommandQueue.kt | 4 +- .../ui/compose/pump/PumpActivityDialog.kt | 5 +- .../pump/PumpActivityDialogPreviews.kt | 5 +- .../compose/pump/PumpCommunicationStatus.kt | 9 +-- .../ui/compose/pump/PumpOverviewModels.kt | 3 +- .../ui/compose/pump/PumpOverviewScreen.kt | 3 +- .../ui/compose/pump/PumpOverviewScreenTest.kt | 3 +- .../aps/DetermineBasalResult.kt | 27 --------- .../queue/CommandQueueImplementation.kt | 30 ++++++---- .../queue/CommandQueueImplementationTest.kt | 55 +++++++++++++++++++ .../app/aaps/ui/compose/main/MainScreen.kt | 3 +- .../ui/compose/overview/OverviewScreen.kt | 3 +- 19 files changed, 141 insertions(+), 145 deletions(-) diff --git a/app/src/androidTest/kotlin/app/aaps/e2e/AbstractDanaEmulatorUiTest.kt b/app/src/androidTest/kotlin/app/aaps/e2e/AbstractDanaEmulatorUiTest.kt index bb68bdb5dbf5..e34ed662231e 100644 --- a/app/src/androidTest/kotlin/app/aaps/e2e/AbstractDanaEmulatorUiTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/e2e/AbstractDanaEmulatorUiTest.kt @@ -22,6 +22,8 @@ import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.profile.ProfileRepository import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.StringKey @@ -32,7 +34,6 @@ import app.aaps.plugins.aps.utils.StaticInjector import app.aaps.pump.dana.DanaPump import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking -import org.json.JSONArray import org.junit.After import org.junit.Before import java.io.File @@ -192,14 +193,13 @@ abstract class AbstractDanaEmulatorUiTest { * deliberately invalid — fill them in or the profile switch below is rejected as invalid. */ private fun seedLocalProfile() { - val profile = profileRepository.newDraft().apply { - mgdl = true - ic = JSONArray(singleValue(10.0)) - isf = JSONArray(singleValue(50.0)) - basal = JSONArray(singleValue(0.5)) - targetLow = JSONArray(singleValue(100.0)) - targetHigh = JSONArray(singleValue(110.0)) - } + val profile = profileRepository.newDraft().copy( + mgdl = true, + ic = singleBlock(10.0), + isf = singleBlock(50.0), + basal = singleBlock(0.5), + target = singleTargetBlock(100.0, 110.0) + ) check(profile.name == PROFILE_NAME) { "Expected the draft to be named $PROFILE_NAME, got ${profile.name}" } runBlocking { profileRepository.add(profile) }.getOrThrow() } @@ -242,9 +242,6 @@ abstract class AbstractDanaEmulatorUiTest { } /** The profile-editor JSON shape: a single all-day value. */ - private fun singleValue(value: Double) = - """[{"time":"00:00","timeAsSeconds":0,"value":$value}]""" - /** Polls [supplier] until it returns non-null or [timeoutMs] elapses. */ private fun awaitNotNull(timeoutMs: Long, supplier: () -> T?): T? { val end = SystemClock.uptimeMillis() + timeoutMs diff --git a/app/src/androidTest/kotlin/app/aaps/e2e/DanaRPairWizardUiTest.kt b/app/src/androidTest/kotlin/app/aaps/e2e/DanaRPairWizardUiTest.kt index d20f13ad15dc..1d2e86a8b931 100644 --- a/app/src/androidTest/kotlin/app/aaps/e2e/DanaRPairWizardUiTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/e2e/DanaRPairWizardUiTest.kt @@ -27,6 +27,8 @@ import app.aaps.core.interfaces.profile.ProfileRepository import app.aaps.core.interfaces.pump.Pump import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.keys.BooleanComposedKey import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.StringKey @@ -42,7 +44,6 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.runBlocking -import org.json.JSONArray import org.junit.After import org.junit.Before import org.junit.Rule @@ -172,14 +173,13 @@ class DanaRPairWizardUiTest { } private fun seedLocalProfile() { - val profile = profileRepository.newDraft().apply { - mgdl = true - ic = JSONArray(singleValue(10.0)) - isf = JSONArray(singleValue(50.0)) - basal = JSONArray(singleValue(0.5)) - targetLow = JSONArray(singleValue(100.0)) - targetHigh = JSONArray(singleValue(110.0)) - } + val profile = profileRepository.newDraft().copy( + mgdl = true, + ic = singleBlock(10.0), + isf = singleBlock(50.0), + basal = singleBlock(0.5), + target = singleTargetBlock(100.0, 110.0) + ) runBlocking { profileRepository.add(profile) }.getOrThrow() } @@ -199,8 +199,6 @@ class DanaRPairWizardUiTest { checkNotNull(switch) { "Could not activate the seeded local profile" } } - private fun singleValue(value: Double) = """[{"time":"00:00","timeAsSeconds":0,"value":$value}]""" - // ---- ui helpers (same contract as DanaRSPairWizardUiTest) ----------------------------------- private fun byText(s: String): BySelector = By.text(Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) diff --git a/app/src/androidTest/kotlin/app/aaps/e2e/DanaRSPairWizardUiTest.kt b/app/src/androidTest/kotlin/app/aaps/e2e/DanaRSPairWizardUiTest.kt index 07ff63c29e12..c1c69d3a0098 100644 --- a/app/src/androidTest/kotlin/app/aaps/e2e/DanaRSPairWizardUiTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/e2e/DanaRSPairWizardUiTest.kt @@ -29,6 +29,8 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.pump.ble.PairingState import app.aaps.core.interfaces.pump.ble.PairingStep import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.keys.BooleanComposedKey import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.StringKey @@ -43,7 +45,6 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.runBlocking -import org.json.JSONArray import org.junit.After import org.junit.Before import org.junit.Rule @@ -195,14 +196,13 @@ class DanaRSPairWizardUiTest { } private fun seedLocalProfile() { - val profile = profileRepository.newDraft().apply { - mgdl = true - ic = JSONArray(singleValue(10.0)) - isf = JSONArray(singleValue(50.0)) - basal = JSONArray(singleValue(0.5)) - targetLow = JSONArray(singleValue(100.0)) - targetHigh = JSONArray(singleValue(110.0)) - } + val profile = profileRepository.newDraft().copy( + mgdl = true, + ic = singleBlock(10.0), + isf = singleBlock(50.0), + basal = singleBlock(0.5), + target = singleTargetBlock(100.0, 110.0) + ) runBlocking { profileRepository.add(profile) }.getOrThrow() } @@ -227,8 +227,6 @@ class DanaRSPairWizardUiTest { preferences.put(BooleanComposedKey.ConfigBuilderEnabled, "PUMP_VirtualPumpPlugin", value = false) } - private fun singleValue(value: Double) = """[{"time":"00:00","timeAsSeconds":0,"value":$value}]""" - // ---- ui helpers (same contract as DanaRsEmulatorUiTest) ------------------------------------- private fun byText(s: String): BySelector = By.text(Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) diff --git a/app/src/androidTest/kotlin/app/aaps/e2e/EquilActivationWizardUiTest.kt b/app/src/androidTest/kotlin/app/aaps/e2e/EquilActivationWizardUiTest.kt index a0f9bc27eb6d..9a17aafc5653 100644 --- a/app/src/androidTest/kotlin/app/aaps/e2e/EquilActivationWizardUiTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/e2e/EquilActivationWizardUiTest.kt @@ -27,6 +27,8 @@ import app.aaps.core.interfaces.profile.ProfileRepository import app.aaps.core.interfaces.pump.ble.BleTransport import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.keys.BooleanComposedKey import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.BooleanNonKey @@ -41,7 +43,6 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.runBlocking -import org.json.JSONArray import org.junit.After import org.junit.Before import org.junit.Rule @@ -222,14 +223,13 @@ class EquilActivationWizardUiTest { } private fun seedLocalProfile() { - val profile = profileRepository.newDraft().apply { - mgdl = true - ic = JSONArray(singleValue(10.0)) - isf = JSONArray(singleValue(50.0)) - basal = JSONArray(singleValue(0.5)) - targetLow = JSONArray(singleValue(100.0)) - targetHigh = JSONArray(singleValue(110.0)) - } + val profile = profileRepository.newDraft().copy( + mgdl = true, + ic = singleBlock(10.0), + isf = singleBlock(50.0), + basal = singleBlock(0.5), + target = singleTargetBlock(100.0, 110.0) + ) runBlocking { profileRepository.add(profile) }.getOrThrow() } @@ -249,8 +249,6 @@ class EquilActivationWizardUiTest { checkNotNull(switch) { "Could not activate the seeded local profile" } } - private fun singleValue(value: Double) = """[{"time":"00:00","timeAsSeconds":0,"value":$value}]""" - // ---- ui helpers (same contract as DanaRSPairWizardUiTest) ----------------------------------- private fun byText(s: String): BySelector = By.text(Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE)) diff --git a/app/src/androidTest/kotlin/app/aaps/e2e/EquilEmulatorActivationTest.kt b/app/src/androidTest/kotlin/app/aaps/e2e/EquilEmulatorActivationTest.kt index 6806a2768418..928f11def068 100644 --- a/app/src/androidTest/kotlin/app/aaps/e2e/EquilEmulatorActivationTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/e2e/EquilEmulatorActivationTest.kt @@ -31,6 +31,8 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.HardLimits +import app.aaps.core.objects.extensions.singleBlock +import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.keys.BooleanComposedKey import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.BooleanNonKey @@ -61,7 +63,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.json.JSONArray import org.junit.After import org.junit.Rule import org.junit.Test @@ -462,14 +463,13 @@ class EquilEmulatorActivationTest { } private fun seedLocalProfile() { - val profile = profileRepository.newDraft().apply { - mgdl = true - ic = JSONArray(singleValue(10.0)) - isf = JSONArray(singleValue(50.0)) - basal = JSONArray(singleValue(0.5)) - targetLow = JSONArray(singleValue(100.0)) - targetHigh = JSONArray(singleValue(110.0)) - } + val profile = profileRepository.newDraft().copy( + mgdl = true, + ic = singleBlock(10.0), + isf = singleBlock(50.0), + basal = singleBlock(0.5), + target = singleTargetBlock(100.0, 110.0) + ) runBlocking { profileRepository.add(profile) }.getOrThrow() } @@ -505,9 +505,6 @@ class EquilEmulatorActivationTest { return null } - private fun singleValue(value: Double) = - """[{"time":"00:00","timeAsSeconds":0,"value":$value}]""" - private fun clearAllSharedPrefs() { val ctx = instrumentation.targetContext File(ctx.applicationInfo.dataDir, "shared_prefs").listFiles()?.forEach { f -> diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt index 31ab3c1fc5fc..385115077116 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt @@ -1,6 +1,5 @@ package app.aaps.plugins.aps.openAPS -import android.text.Spanned import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -30,7 +29,6 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertedToAbsolute import app.aaps.core.objects.extensions.convertedToPercent import app.aaps.core.ui.R -import app.aaps.core.utils.HtmlHelper import dagger.android.HasAndroidInjector import kotlinx.coroutines.runBlocking import org.json.JSONObject @@ -123,35 +121,6 @@ open class APSResultObject(protected val injector: HasAndroidInjector) : APSResu } else rh.gs(R.string.nochangerequested) } - override suspend fun resultAsSpanned(): Spanned = HtmlHelper.fromHtml(resultAsHtmlString()) - override suspend fun resultAsHtmlString(): String { - val pump = activePlugin.activePump - if (isChangeRequested()) { - // rate - var ret: String = - if (rate == 0.0 && duration == 0) rh.gs(R.string.cancel_temp) + "
" - else if (rate == -1.0) rh.gs(R.string.let_temp_basal_run) + "
" - else if (usePercent) "" + rh.gs(R.string.rate) + ": " + decimalFormatter.to2Decimal(percent.toDouble()) + "% " + - "(" + decimalFormatter.to2Decimal(percent * ch.fromPump(pump.baseBasalRate) / 100.0) + " U/h)
" + - "" + rh.gs(R.string.duration) + ": " + decimalFormatter.to2Decimal(duration.toDouble()) + " min
" - else "" + rh.gs(R.string.rate) + ": " + decimalFormatter.to2Decimal(rate) + " U/h " + - "(" + decimalFormatter.to2Decimal(rate / ch.fromPump(pump.baseBasalRate) * 100.0) + "%)
" + - "" + rh.gs(R.string.duration) + ": " + decimalFormatter.to2Decimal(duration.toDouble()) + " min
" - - // smb - if (smb != 0.0) ret += "" + "SMB" + ": " + decimalFormatter.toPumpSupportedBolus(smb, activePlugin.activePump.pumpDescription.bolusStep) + "
" - if (isCarbsRequired) { - ret += "$carbsRequiredText
" - } - - // reason - ret += "" + rh.gs(R.string.reason) + ": " + reason.replace("<", "<").replace(">", ">") - return ret - } - return if (isCarbsRequired) carbsRequiredText - else rh.gs(R.string.nochangerequested) - } - override fun newAndClone(): APSResult { val newResult = APSResultObject(injector) doClone(newResult) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt index 85d8718d9938..ec35b660e1da 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.aps -import android.text.Spanned import app.aaps.core.data.model.GV import app.aaps.core.interfaces.constraints.Constraint import org.json.JSONObject @@ -52,8 +51,6 @@ interface APSResult { val iob: IobTotal? get() = iobData?.get(0) suspend fun resultAsString(): String - suspend fun resultAsSpanned(): Spanned - suspend fun resultAsHtmlString(): String fun newAndClone(): APSResult fun json(): JSONObject? fun predictions(): Predictions? diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt index 2a9e3a93fa1f..8146a416afd0 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.queue -import android.text.Spanned +import androidx.compose.ui.text.AnnotatedString import app.aaps.core.interfaces.profile.EffectiveProfile import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.DetailedBolusInfo @@ -57,6 +57,6 @@ interface CommandQueue { suspend fun customCommand(customCommand: CustomCommand): PumpEnactResult fun isCustomCommandRunning(customCommandType: Class): Boolean fun isCustomCommandInQueue(customCommandType: Class): Boolean - fun spannedStatus(): Spanned + fun statusAsAnnotated(): AnnotatedString suspend fun isThisProfileSet(requestedProfile: EffectiveProfile): Boolean } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt index e82dacc248c9..49031a70746b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.background @@ -46,7 +47,7 @@ import app.aaps.core.ui.compose.AapsSpacing fun PumpActivityDialog( bolusState: BolusProgressState?, pumpStatus: String, - queueStatus: String?, + queueStatus: AnnotatedString?, isModal: Boolean, onStop: () -> Unit, onDismiss: () -> Unit @@ -101,7 +102,7 @@ fun PumpActivityDialog( internal fun PumpActivityCard( bolusState: BolusProgressState?, pumpStatus: String, - queueStatus: String?, + queueStatus: AnnotatedString?, onStop: () -> Unit, onDismiss: () -> Unit ) { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt index d9a960ac8db1..329ce2d4c9e0 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview @@ -96,7 +97,7 @@ internal fun PreviewBolusIndeterminate() { stopDeliveryEnabled = false ), pumpStatus = "Connecting for 5s", - queueStatus = "BOLUS 2.50U", + queueStatus = AnnotatedString("BOLUS 2.50U"), onStop = {}, onDismiss = {} ) @@ -135,7 +136,7 @@ internal fun PreviewPumpStatusOnly() { PumpActivityCard( bolusState = null, pumpStatus = "Handshaking", - queueStatus = "READSTATUS", + queueStatus = AnnotatedString("READSTATUS"), onStop = {}, onDismiss = {} ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt index bd191cc4dbcb..d910a6792927 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.rx.bus.RxBus @@ -30,8 +31,8 @@ class PumpCommunicationStatus( private val _statusBanner = MutableStateFlow(null) val statusBannerFlow: StateFlow = _statusBanner.asStateFlow() - private val _queueStatus = MutableStateFlow(null) - val queueStatusFlow: StateFlow = _queueStatus.asStateFlow() + private val _queueStatus = MutableStateFlow(null) + val queueStatusFlow: StateFlow = _queueStatus.asStateFlow() /** Emits whenever communication status or queue changes. */ val refreshTrigger: MutableStateFlow = MutableStateFlow(0L) @@ -47,7 +48,7 @@ class PumpCommunicationStatus( rxBus.toFlow(EventQueueChanged::class.java) .onEach { - _queueStatus.value = commandQueue.spannedStatus().toString().takeIf { it.isNotEmpty() } + _queueStatus.value = commandQueue.statusAsAnnotated().takeIf { it.isNotEmpty() } refreshTrigger.value = System.currentTimeMillis() } .launchIn(scope) @@ -57,5 +58,5 @@ class PumpCommunicationStatus( fun statusBanner(): StatusBanner? = _statusBanner.value /** Returns the current command queue status text. */ - fun queueStatus(): String? = _queueStatus.value + fun queueStatus(): AnnotatedString? = _queueStatus.value } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt index 0569373754d3..1e9b271a2f16 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.vector.ImageVector @@ -15,7 +16,7 @@ data class PumpOverviewUiState( val infoRows: List = emptyList(), val primaryActions: List = emptyList(), val managementActions: List = emptyList(), - val queueStatus: String? = null + val queueStatus: AnnotatedString? = null ) /** diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt index 73b581c8efd7..c172d4e0ff2c 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -82,7 +83,7 @@ fun PumpOverviewScreen( // ── Communication status card (banner + queue in one card) ──────────────── @Composable -private fun CommunicationStatusCard(banner: StatusBanner?, queueStatus: String?) { +private fun CommunicationStatusCard(banner: StatusBanner?, queueStatus: AnnotatedString?) { if (banner == null && queueStatus == null) return val (bgColor, fgColor) = when (banner?.level) { diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt index 36220abaff33..704ae64337f1 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.pump +import androidx.compose.ui.text.AnnotatedString import androidx.compose.material3.MaterialTheme import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.v2.createComposeRule @@ -51,7 +52,7 @@ class PumpOverviewScreenTest { fun showsQueueStatus() { val state = PumpOverviewUiState( statusBanner = StatusBanner(text = "Idle"), - queueStatus = "Reading status" + queueStatus = AnnotatedString("Reading status") ) compose.setContent { MaterialTheme { PumpOverviewScreen(state = state) } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt b/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt index cb861fc8a681..54c9639e4e2b 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt @@ -1,6 +1,5 @@ package app.aaps.implementation.aps -import android.text.Spanned import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -32,7 +31,6 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertedToAbsolute import app.aaps.core.objects.extensions.convertedToPercent import app.aaps.core.ui.R -import app.aaps.core.utils.HtmlHelper import org.json.JSONObject import javax.inject.Inject import javax.inject.Provider @@ -142,31 +140,6 @@ class DetermineBasalResult @Inject constructor( } else rh.gs(R.string.nochangerequested) } - override suspend fun resultAsSpanned(): Spanned = HtmlHelper.fromHtml(resultAsHtmlString()) - override suspend fun resultAsHtmlString(): String { - val pump = activePlugin.activePump - if (isChangeRequested()) { - // rate - var ret: String = - if (rate == 0.0 && duration == 0) rh.gs(R.string.cancel_temp) + "
" - else if (rate == -1.0) rh.gs(R.string.let_temp_basal_run) + "
" - else if (usePercent) rh.gs(R.string.percent_rate_duration_formatted, percent.toDouble(), percent * ch.fromPump(pump.baseBasalRate) / 100.0, duration) - else rh.gs(R.string.rate_percent_duration_formatted, rate, rate / ch.fromPump(pump.baseBasalRate) * 100.0, duration) - - // smb - if (smb != 0.0) ret += "" + "SMB" + ": " + decimalFormatter.toPumpSupportedBolus(smb, activePlugin.activePump.pumpDescription.bolusStep) + "
" - if (isCarbsRequired) { - ret += "$carbsRequiredText
" - } - - // reason - ret += "" + rh.gs(R.string.reason) + ": " + reason.replace("<", "<").replace(">", ">") - return ret - } - return if (isCarbsRequired) carbsRequiredText - else rh.gs(R.string.nochangerequested) - } - override fun newAndClone(): APSResult = apsResultProvider.get().with(result) override fun json(): JSONObject { reportNonFiniteResultFields() diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index fcadba17c769..ce86fb5724f8 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -1,6 +1,10 @@ package app.aaps.implementation.queue -import android.text.Spanned +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import app.aaps.annotations.OpenForTesting import app.aaps.core.data.model.BS import app.aaps.core.data.model.EPS @@ -44,7 +48,6 @@ import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.getCustomizedName import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.objects.runningMode.PumpCommandGate -import app.aaps.core.utils.HtmlHelper import app.aaps.implementation.R import app.aaps.implementation.queue.commands.CommandBolus import app.aaps.implementation.queue.commands.CommandCancelExtendedBolus @@ -769,22 +772,25 @@ class CommandQueueImplementation @Inject constructor( } } - override fun spannedStatus(): Spanned { - var s = "" - var line = 0 + /** + * The running command in bold, then one queued command per line. + * + * This used to build an HTML string and hand it to `Html.fromHtml`, but the only caller flattened + * the result with `toString()` - which keeps the line breaks and silently drops the bold, because + * a String cannot carry spans. Building the styling directly means the emphasis on the command + * actually executing now survives all the way to the screen. + */ + override fun statusAsAnnotated(): AnnotatedString = buildAnnotatedString { val perf = performing if (perf != null) { - s += "" + perf.status() + "" - line++ + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(perf.status()) } } synchronized(queue) { - for (i in queue.indices) { - if (line != 0) s += "
" - s += queue[i].status() - line++ + for (command in queue) { + if (length != 0) append('\n') + append(command.status()) } } - return HtmlHelper.fromHtml(s) } override suspend fun isThisProfileSet(requestedProfile: EffectiveProfile): Boolean { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index 55ca409a036f..a1df5c88efcf 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -2,6 +2,7 @@ package app.aaps.implementation.queue import android.content.Context import android.os.PowerManager +import androidx.compose.ui.text.font.FontWeight import app.aaps.core.data.model.BS import app.aaps.core.interfaces.alerts.LocalAlertUtils import app.aaps.core.interfaces.configuration.Config @@ -844,6 +845,60 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { assertThat(commandQueue.size()).isEqualTo(1) } + /** + * The queue status carries real styling now. + * + * It used to be assembled as an HTML string and handed to `Html.fromHtml`, but the only consumer + * flattened that with `toString()` - which keeps the line breaks and silently drops the bold, + * because a String cannot carry spans. So the emphasis on the command actually executing never + * reached the screen. These two tests pin both halves of the replacement: one line per command, + * and the running one actually bold. + */ + /** + * The two command captions the status list is built from. + * + * `read_status` takes an argument, and the shared mock formats it from the NO-ARG `gs(id)`, so + * this has to hand back the template rather than the finished string. + */ + private fun stubStatusCaptions() { + whenever(rh.gs(app.aaps.core.ui.R.string.read_status)).thenReturn("READSTATUS %1\$s") + whenever(rh.gs(app.aaps.core.ui.R.string.load_events)).thenReturn("LOAD EVENTS") + } + + @Test + fun `queued commands are listed one per line`() = runTest { + stubStatusCaptions() + backgroundScope.launch { commandQueue.readStatus("test") } + yield() + + val status = commandQueue.statusAsAnnotated() + + assertThat(status.text).isNotEmpty() + // Only one command queued and nothing running, so there is no separator yet. + assertThat(status.text).doesNotContain("\n") + assertThat(status.spanStyles).isEmpty() + } + + @Test + fun `the executing command is bold, the queued ones are not`() = runTest { + stubStatusCaptions() + backgroundScope.launch { commandQueue.readStatus("test") } + yield() + // pickup() moves the head of the queue into `performing`, which is what the bold marks. + commandQueue.pickup() + backgroundScope.launch { commandQueue.loadEvents() } + yield() + + val status = commandQueue.statusAsAnnotated() + + assertThat(status.text).contains("\n") + val bold = status.spanStyles.filter { it.item.fontWeight == FontWeight.Bold } + assertThat(bold).hasSize(1) + // Exactly the first line - the running command - and nothing after it. + assertThat(bold.single().start).isEqualTo(0) + assertThat(bold.single().end).isEqualTo(status.text.indexOf('\n')) + } + private suspend fun stubActiveMode(mode: app.aaps.core.data.model.RM.Mode) { whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn( app.aaps.core.data.model.RM(timestamp = 0, mode = mode, duration = 0L) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainScreen.kt index 268c361f2247..e56bc937c408 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.main +import androidx.compose.ui.text.AnnotatedString import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically @@ -139,7 +140,7 @@ fun MainScreen( // Pump activity bolusState: BolusProgressState? = null, pumpStatusText: String = "", - queueStatusText: String? = null, + queueStatusText: AnnotatedString? = null, isPumpCommunicating: Boolean = false, onStopBolus: () -> Unit = {}, modifier: Modifier = Modifier diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreen.kt index 1f8588944e33..db8eaf812243 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreen.kt @@ -1,5 +1,6 @@ package app.aaps.ui.compose.overview +import androidx.compose.ui.text.AnnotatedString import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -90,7 +91,7 @@ fun OverviewScreen( fabBottomOffset: Dp = 0.dp, bolusState: BolusProgressState? = null, pumpStatusText: String = "", - queueStatusText: String? = null, + queueStatusText: AnnotatedString? = null, isPumpCommunicating: Boolean = false, onStopBolus: () -> Unit = {}, modifier: Modifier = Modifier From a7b79f947c300b848942858b13e5c4483cc2784d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 17:48:50 +0200 Subject: [PATCH 066/146] Four core:interfaces files off JVM-only types --- .../core/interfaces/nsclient/NSClientLog.kt | 9 +++++++-- .../core/interfaces/pump/BolusProgressData.kt | 19 +++++++++++-------- .../core/interfaces/rx/weardata/EventData.kt | 11 +++++------ .../core/interfaces/utils/MidnightTime.kt | 5 ++--- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt index f3a1d482b8d2..6e76dca6b6be 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt @@ -1,9 +1,12 @@ package app.aaps.core.interfaces.nsclient import kotlinx.serialization.json.JsonElement -import java.util.concurrent.atomic.AtomicLong +import kotlin.concurrent.atomics.AtomicLong +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.fetchAndIncrement import kotlin.time.Clock +@OptIn(ExperimentalAtomicApi::class) class NSClientLog( val action: String, val logText: String? = null, @@ -11,10 +14,12 @@ class NSClientLog( ) { val date: Long = Clock.System.now().toEpochMilliseconds() - val id: Long = idCounter.getAndIncrement() + val id: Long = idCounter.fetchAndIncrement() companion object { + // kotlin.concurrent.atomics rather than java.util.concurrent: same semantics, but it exists on + // every target, so this class can move to commonMain. private val idCounter = AtomicLong(0) } } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt index 6410e48c86e1..959ed3ec4dab 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt @@ -11,7 +11,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.util.concurrent.atomic.AtomicLong +import kotlin.concurrent.atomics.AtomicLong +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.atomics.incrementAndFetch /** * Core-controlled bolus progress state. @@ -24,6 +26,7 @@ import java.util.concurrent.atomic.AtomicLong * instead (`ImplementationModule.provideBolusProgressData`, which keeps the singleton scope and * supplies the application-scoped CoroutineScope). */ +@OptIn(ExperimentalAtomicApi::class) class BolusProgressData( val ch: ConcentrationHelper, private val appScope: CoroutineScope, @@ -37,7 +40,7 @@ class BolusProgressData( private val generation = AtomicLong(0) /** Snapshot of the current generation, captured by the client watchdog when it arms (see [markStalled]). */ - val currentGeneration: Long get() = generation.get() + val currentGeneration: Long get() = generation.load() /** * Called by CommandQueue before bolus delivery starts. @@ -47,7 +50,7 @@ class BolusProgressData( * wipe the progress state of a NEWER bolus that has already started (see [clear]). */ fun start(insulin: Double, isSMB: Boolean, isPriming: Boolean = false): Long { - val gen = generation.incrementAndGet() + val gen = generation.incrementAndFetch() _state.value = BolusProgressState( insulin = insulin, isSMB = isSMB, @@ -129,7 +132,7 @@ class BolusProgressData( * mirror, whose relayed frames are timestamp-ordered. A per-command MASTER bolus MUST use the generation-scoped * [completeAndAutoClear] overload instead (same rationale as [clear] vs [clear]). */ - fun completeAndAutoClear() = completeAndAutoClear(generation.get()) + fun completeAndAutoClear() = completeAndAutoClear(generation.load()) /** * Generation-scoped completion for a single master bolus command (mirror of [clear]). Guards BOTH the immediate @@ -140,11 +143,11 @@ class BolusProgressData( */ fun completeAndAutoClear(expectedGeneration: Long) { _state.update { current -> - if (current != null && generation.get() == expectedGeneration) current.copy(percent = 100, stalled = false) else current + if (current != null && generation.load() == expectedGeneration) current.copy(percent = 100, stalled = false) else current } appScope.launch { delay(AUTO_CLEAR_DELAY_MS) - if (generation.get() == expectedGeneration) _state.value = null + if (generation.load() == expectedGeneration) _state.value = null } } @@ -179,7 +182,7 @@ class BolusProgressData( * a no-op instead of wiping the state it just installed. */ fun clear(expectedGeneration: Long) { - _state.update { current -> if (generation.get() == expectedGeneration) null else current } + _state.update { current -> if (generation.load() == expectedGeneration) null else current } } /** @@ -197,7 +200,7 @@ class BolusProgressData( */ fun markStalled(expectedGeneration: Long) { _state.update { - if (it != null && it.percent < 100 && generation.get() == expectedGeneration) it.copy(stalled = true) + if (it != null && it.percent < 100 && generation.load() == expectedGeneration) it.copy(stalled = true) else it } } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt index a5f3753b41b1..06915c6ca87f 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt @@ -2,11 +2,10 @@ package app.aaps.core.interfaces.rx.weardata import app.aaps.core.interfaces.rx.events.Event import kotlinx.serialization.ExperimentalSerializationApi +import kotlin.time.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf -import java.util.Date -import java.util.Objects import kotlin.time.Clock @Serializable @@ -64,7 +63,7 @@ sealed class EventData : Event() { } override fun hashCode(): Int { - return Objects.hash(timeStamp, fingerprint) + return 31 * timeStamp.hashCode() + fingerprint.hashCode() } } @@ -164,7 +163,7 @@ sealed class EventData : Event() { ) : EventData() { override fun toString() = - "HR ${beatsPerMinute.toInt()} at ${Date(timestamp)} for ${duration / 1000.0}sec $device" + "HR ${beatsPerMinute.toInt()} at ${Instant.fromEpochMilliseconds(timestamp)} for ${duration / 1000.0}sec $device" } @Serializable @@ -181,7 +180,7 @@ sealed class EventData : Event() { ) : EventData() { override fun toString() = - "STEPS 5min: $steps5min, 10min: $steps10min, 15min: $steps15min, 30min: $steps30min, 60min: $steps60min, 180min: $steps180min at ${Date(timestamp)} for ${duration / 1000.0}sec $device" + "STEPS 5min: $steps5min, 10min: $steps10min, 15min: $steps15min, 30min: $steps30min, 60min: $steps60min, 180min: $steps180min at ${Instant.fromEpochMilliseconds(timestamp)} for ${duration / 1000.0}sec $device" } @Serializable @@ -311,7 +310,7 @@ sealed class EventData : Event() { } override fun hashCode(): Int { - return Objects.hash(timeStamp, color) + return 31 * timeStamp.hashCode() + color.hashCode() } override fun compareTo(other: SingleBg): Int { diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt index d19bb38b0f66..3c45ae58b507 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.utils -import androidx.annotation.VisibleForTesting import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime @@ -21,7 +20,7 @@ object MidnightTime { * * Note this cache has never actually filled - see [calc]. */ - @VisibleForTesting + // Visible for testing only. val times = HashMap() private const val THRESHOLD = 100000 @@ -97,7 +96,7 @@ object MidnightTime { return date.atStartOfDayIn(tz).toEpochMilliseconds() } - @VisibleForTesting + // Visible for testing only. fun resetCache() { times.clear() } From d8b74a5eb2c30b8f4363e670fa6c2558231bb584 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 18:26:13 +0200 Subject: [PATCH 067/146] Alarm sounds as an enum instead of raw resource ids --- .../kotlin/app/aaps/ComposeMainActivity.kt | 5 ++- .../aaps/implementations/UiInteractionImpl.kt | 22 ++++----- .../notifications/AapsNotification.kt | 3 +- .../interfaces/notifications/AlarmIntent.kt | 4 +- .../interfaces/notifications/AlarmSound.kt | 33 ++++++++++++++ .../notifications/AlarmSoundPlayer.kt | 5 +-- .../notifications/NotificationManager.kt | 7 ++- .../aaps/core/interfaces/ui/UiInteraction.kt | 6 +-- .../app/aaps/core/ui/AlarmSoundResources.kt | 21 +++++++++ .../alerts/LocalAlertUtilsImpl.kt | 5 ++- .../AlarmNotificationManager.kt | 45 ++++++++++--------- .../AlarmSoundPlayerImpl.kt | 20 ++++----- .../bolus/WizardBolusExecutorImpl.kt | 3 +- .../notifications/NotificationManagerImpl.kt | 30 ++++++------- .../queue/CommandQueueImplementation.kt | 5 ++- .../implementation/utils/HardLimitsImpl.kt | 3 +- .../bolus/WizardBolusExecutorImplTest.kt | 5 ++- .../queue/CommandQueueImplementationTest.kt | 5 ++- .../automation/TimerReminderReceiver.kt | 3 +- .../nsclientV3/NsIncomingDataProcessor.kt | 3 +- .../clientcontrol/ClientControlRoundTrip.kt | 3 +- .../nsclientV3/services/NSClientV3Service.kt | 5 ++- .../ClientControlRoundTripTest.kt | 3 +- .../services/DanaRv2ExecutionService.kt | 3 +- .../pump/danars/services/DanaRSService.kt | 3 +- .../pump/diaconn/service/BLECommonService.kt | 7 +-- .../pump/diaconn/service/DiaconnG8Service.kt | 3 +- .../aaps/pump/eopatch/alarm/AlarmManager.kt | 5 ++- .../app/aaps/pump/eopatch/ble/PatchManager.kt | 3 +- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 13 +++--- .../aaps/pump/equil/manager/EquilManager.kt | 3 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 3 +- .../pump/medtrum/services/MedtrumService.kt | 11 ++--- .../omnipod/dash/OmnipodDashPumpPlugin.kt | 31 ++++++------- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 5 ++- .../eros/manager/AapsOmnipodErosManager.java | 25 ++++++----- .../app/aaps/ui/activities/ErrorActivity.kt | 9 ++-- 37 files changed, 224 insertions(+), 144 deletions(-) create mode 100644 core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt create mode 100644 core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt diff --git a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt index e0dd6ae74c41..fca72aab6c9b 100644 --- a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt +++ b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt @@ -81,6 +81,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.maintenance.FileListProvider import app.aaps.core.interfaces.navigation.ElementType +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -643,7 +644,7 @@ class ComposeMainActivity : AppCompatActivity() { isSimpleMode = state.isSimpleMode, onNavigate = { request -> handleNavigationRequest(request, navController) }, onActionsError = { comment, title -> - uiInteraction.runAlarm(comment, title, app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(comment, title, AlarmSound.BOLUS_ERROR) }, ) @@ -799,7 +800,7 @@ class ComposeMainActivity : AppCompatActivity() { visibilityContext = visibilityContext, onNavigationRequest = { request, nc -> handleNavigationRequest(request, nc) }, onShowDeliveryError = { comment, titleResId -> - uiInteraction.runAlarm(comment, rh.gs(titleResId), app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(comment, rh.gs(titleResId), AlarmSound.BOLUS_ERROR) }, withProtection = { protection, action -> withProtection(protection, action) }, requestEditModeAuthorization = { onGranted -> diff --git a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt index 6c0e209cf7cf..c37d3d6dd493 100644 --- a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt +++ b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt @@ -4,7 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Looper import android.widget.Toast -import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import app.aaps.ComposeMainActivity @@ -45,7 +45,7 @@ class UiInteractionImpl @Inject constructor( override val mainActivity: Class<*> = ComposeMainActivity::class.java override val errorHelperActivity: Class<*> = ErrorActivity::class.java - override fun runAlarm(status: String, title: String, @RawRes soundId: Int) { + override fun runAlarm(status: String, title: String, sound: AlarmSound?) { // Persist the error as an announcement at fire time — gated by the NS-announcement // preference + APS build. Done here (not in ErrorActivity) so the record is written for // every alarm with the true trigger time, regardless of whether/how it is later @@ -66,8 +66,8 @@ class UiInteractionImpl @Inject constructor( // from non-main threads. From those contexts we skip the foreground-direct optimization // entirely and use the FSI path, which is safe from any thread. if (Looper.myLooper() != Looper.getMainLooper()) { - aapsLogger.debug(LTag.CORE, "runAlarm (off-main → FSI): $title - $status (sound=$soundId)") - alarmNotificationManager.postFullScreenAlarm(status = status, title = title, soundId = soundId) + aapsLogger.debug(LTag.CORE, "runAlarm (off-main → FSI): $title - $status (sound=$sound)") + alarmNotificationManager.postFullScreenAlarm(status = status, title = title, sound = sound) return } @@ -77,9 +77,9 @@ class UiInteractionImpl @Inject constructor( // • Activity opens instantly, owns ramped audio from 0. // • Works because the caller's process is already foreground (Android's // background-activity-start restriction does not apply). - aapsLogger.debug(LTag.CORE, "runAlarm (foreground direct): $title - $status (sound=$soundId)") + aapsLogger.debug(LTag.CORE, "runAlarm (foreground direct): $title - $status (sound=$sound)") val intent = Intent(context, errorHelperActivity).apply { - putExtra(AlarmIntent.EXTRA_SOUND_ID, soundId) + putExtra(AlarmIntent.EXTRA_SOUND, sound?.name) putExtra(AlarmIntent.EXTRA_STATUS, status) putExtra(AlarmIntent.EXTRA_TITLE, title) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) @@ -90,14 +90,14 @@ class UiInteractionImpl @Inject constructor( // Defensive: if the activity start is rejected for any reason, fall back to // the FSI notification path so the alert is never silently lost. aapsLogger.error(LTag.CORE, "runAlarm: direct startActivity failed, falling back to FSI", ex) - postFsiFallback(status, title, soundId) + postFsiFallback(status, title, sound) } } else { // Background path — FSI notification. Android auto-launches the activity on // lockscreen/idle, or shows a heads-up (with channel sound) when the user is // active in another app. - aapsLogger.debug(LTag.CORE, "runAlarm (background via FSI): $title - $status (sound=$soundId)") - alarmNotificationManager.postFullScreenAlarm(status = status, title = title, soundId = soundId) + aapsLogger.debug(LTag.CORE, "runAlarm (background via FSI): $title - $status (sound=$sound)") + alarmNotificationManager.postFullScreenAlarm(status = status, title = title, sound = sound) } } @@ -116,8 +116,8 @@ class UiInteractionImpl @Inject constructor( * visible signal that something tried to alarm. Best-effort; Toast can also fail (e.g. * if a system overlay permission is denied) but it costs nothing to try. */ - private fun postFsiFallback(status: String, title: String, @RawRes soundId: Int) { - alarmNotificationManager.postFullScreenAlarm(status = status, title = title, soundId = soundId) + private fun postFsiFallback(status: String, title: String, sound: AlarmSound?) { + alarmNotificationManager.postFullScreenAlarm(status = status, title = title, sound = sound) // Toast must be created on the main thread (we are — runAlarm guards above). runCatching { Toast.makeText(context, "ALARM: $title — $status", Toast.LENGTH_LONG).show() diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt index 07986a591b8d..dccf3c07b30c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.notifications -import androidx.annotation.RawRes import kotlin.time.Clock data class AapsNotification( @@ -10,7 +9,7 @@ data class AapsNotification( val level: NotificationLevel, val date: Long = Clock.System.now().toEpochMilliseconds(), val validTo: Long = 0L, - @RawRes val soundRes: Int? = null, + val sound: AlarmSound? = null, val actions: List = emptyList(), val validityCheck: (() -> Boolean)? = null ) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt index 89483d636f77..cfedbf5cad0a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt @@ -8,7 +8,9 @@ package app.aaps.core.interfaces.notifications object AlarmIntent { /** Raw sound resource id; the activity uses it to play audio with volume ramp. */ - const val EXTRA_SOUND_ID = "soundId" + /** The [app.aaps.core.interfaces.notifications.AlarmSound] name. A name, not a resource id: an id + * is build specific, and this value outlives the process that wrote it. */ + const val EXTRA_SOUND = "sound" /** Alarm status / body text. */ const val EXTRA_STATUS = "status" diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt new file mode 100644 index 000000000000..d04b5f94b7bc --- /dev/null +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt @@ -0,0 +1,33 @@ +package app.aaps.core.interfaces.notifications + +/** + * One of the alarm sounds AAPS ships. + * + * These used to travel as bare `R.raw.*` ints. An id is Android only, so it kept every declaring + * file out of common code, and it carried two further problems that this type removes: + * + * - **Two different "no sound".** `runAlarm` took `soundId: Int = 0` while `AapsNotification` took a + * nullable id, and the player guarded with `if (soundRes == 0) return` in two places. Absence is + * now spelled `null`, once, and the compiler enforces it. + * - **An id in an Intent.** The chosen sound is passed to the full screen alarm activity as an + * extra, and a resource id is a build specific number - the same value means something else in + * the next build. The stable [name] travels instead. + * + * Deliberately a closed set: the sound files live in `:core:ui/res/raw` and there are four of them. + * Adding one means adding an entry here, which makes the `when` in the Android resolver stop + * compiling until it is handled. + */ +enum class AlarmSound { + + /** Standard alarm. */ + ALARM, + + /** Urgent alarm - used for the Nightscout urgent announcement path. */ + URGENT_ALARM, + + /** General error. */ + ERROR, + + /** Bolus delivery failure. */ + BOLUS_ERROR +} diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt index e7d3b37649d0..4fc16b8b5a9d 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.notifications -import androidx.annotation.RawRes import app.aaps.core.interfaces.notifications.AlarmSoundPlayer.Companion.OWNER_FULLSCREEN import app.aaps.core.interfaces.notifications.AlarmSoundPlayer.Companion.OWNER_INTERNAL @@ -23,7 +22,7 @@ import app.aaps.core.interfaces.notifications.AlarmSoundPlayer.Companion.OWNER_I interface AlarmSoundPlayer { /** - * Start looping playback of [soundRes], recording [ownerTag] as the current owner. Any previous + * Start looping playback of [sound], recording [ownerTag] as the current owner. Any previous * playback (from either owner) is stopped first. * * @param postedAtElapsedRealtime [android.os.SystemClock.elapsedRealtime] when an accompanying @@ -32,7 +31,7 @@ interface AlarmSoundPlayer { * auto-launch). Pass 0 (the default) when there is no accompanying channel sound — the * duration probe is then skipped entirely. */ - fun play(@RawRes soundRes: Int, ownerTag: String, postedAtElapsedRealtime: Long = 0L) + fun play(sound: AlarmSound, ownerTag: String, postedAtElapsedRealtime: Long = 0L) /** Stop and release playback **only if** [ownerTag] is the current owner. No-op otherwise. */ fun stop(ownerTag: String) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt index 33411f9bbc3f..1c2da5d205c5 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.notifications -import androidx.annotation.RawRes import androidx.annotation.StringRes import kotlinx.coroutines.flow.StateFlow import kotlin.time.Clock @@ -17,7 +16,7 @@ interface NotificationManager { text: String, level: NotificationLevel = id.defaultLevel, validMinutes: Int = 0, - @RawRes soundRes: Int? = null, + sound: AlarmSound? = null, actions: List = emptyList(), validityCheck: (() -> Boolean)? = null ): NotificationHandle @@ -28,7 +27,7 @@ interface NotificationManager { level: NotificationLevel = id.defaultLevel, date: Long = Clock.System.now().toEpochMilliseconds(), validTo: Long = 0L, - @RawRes soundRes: Int? = null, + sound: AlarmSound? = null, actions: List = emptyList(), validityCheck: (() -> Boolean)? = null ): NotificationHandle @@ -41,7 +40,7 @@ interface NotificationManager { validMinutes: Int = 0, date: Long = Clock.System.now().toEpochMilliseconds(), validTo: Long = 0L, - @RawRes soundRes: Int? = null, + sound: AlarmSound? = null, actions: List = emptyList(), validityCheck: (() -> Boolean)? = null ): NotificationHandle diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt index 5a6988a4e54e..adcf9a8cd653 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.ui -import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound /** * Interface to use activities located in different modules @@ -18,9 +18,9 @@ interface UiInteraction { * Show ErrorHelperActivity and start alarm. * @param status message inside dialog * @param title title of dialog - * @param soundId sound resource. if == 0 alarm is not started + * @param sound alarm sound, or null for a silent alarm */ - fun runAlarm(status: String, title: String, @RawRes soundId: Int = 0) + fun runAlarm(status: String, title: String, sound: AlarmSound? = null) /** * Stops any currently playing alarm (cancels FSI + all sound notifications). diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt new file mode 100644 index 000000000000..cc467890254d --- /dev/null +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt @@ -0,0 +1,21 @@ +package app.aaps.core.ui + +import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound + +/** + * The Android resource behind an [AlarmSound]. + * + * This is the single place that knows sounds are `R.raw` files at all, which is what lets every + * declaring interface stay free of Android. It lives in `:core:ui` because that is the module owning + * the audio files; the `when` is exhaustive, so a new [AlarmSound] fails to compile until it is + * given a file here. + */ +@get:RawRes +val AlarmSound.rawRes: Int + get() = when (this) { + AlarmSound.ALARM -> R.raw.alarm + AlarmSound.URGENT_ALARM -> R.raw.urgentalarm + AlarmSound.ERROR -> R.raw.error + AlarmSound.BOLUS_ERROR -> R.raw.boluserror + } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt index 08c22e8d7a89..78f97190e07c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt @@ -11,6 +11,7 @@ import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.ActivePlugin @@ -67,7 +68,7 @@ class LocalAlertUtilsImpl @Inject constructor( if (preferences.get(BooleanKey.AlertPumpUnreachable)) { aapsLogger.debug(LTag.CORE, "Generating pump unreachable alarm. lastConnection: " + dateUtil.dateAndTimeString(lastConnection) + " isStatusOutdated: true") preferences.put(LocalAlertLongKey.NextPumpDisconnectedAlarm, dateUtil.now() + pumpUnreachableThreshold()) - notificationManager.post(NotificationId.PUMP_UNREACHABLE, R.string.pump_unreachable, soundRes = R.raw.alarm) + notificationManager.post(NotificationId.PUMP_UNREACHABLE, R.string.pump_unreachable, sound = AlarmSound.ALARM) if (preferences.get(BooleanKey.NsClientCreateAnnouncementsFromErrors) && config.APS) appScope.launch { persistenceLayer.insertPumpTherapyEventIfNewByTimestamp( @@ -134,7 +135,7 @@ class LocalAlertUtilsImpl @Inject constructor( && preferences.get(LocalAlertLongKey.NextMissedReadingsAlarm) < dateUtil.now() ) { preferences.put(LocalAlertLongKey.NextMissedReadingsAlarm, dateUtil.now() + missedReadingsThreshold()) - notificationManager.post(NotificationId.BG_READINGS_MISSED, R.string.missed_bg_readings, soundRes = R.raw.alarm) + notificationManager.post(NotificationId.BG_READINGS_MISSED, R.string.missed_bg_readings, sound = AlarmSound.ALARM) if (preferences.get(BooleanKey.NsClientCreateAnnouncementsFromErrors) && config.APS) { appScope.launch { persistenceLayer.insertPumpTherapyEventIfNewByTimestamp( diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt index 8c1750423dcd..52d72aeedcd8 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt @@ -10,7 +10,8 @@ import android.content.Intent import android.media.AudioAttributes import android.net.Uri import android.os.SystemClock -import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound +import app.aaps.core.ui.rawRes import androidx.core.app.NotificationCompat import androidx.core.app.TaskStackBuilder import app.aaps.core.interfaces.logging.AAPSLogger @@ -94,18 +95,18 @@ class AlarmNotificationManager @Inject constructor( */ const val SOUND_ID_OFFSET = 100_000 - private val SOUND_NAMES: Map = mapOf( - app.aaps.core.ui.R.raw.alarm to "alarm", - app.aaps.core.ui.R.raw.boluserror to "boluserror", - app.aaps.core.ui.R.raw.error to "error", - app.aaps.core.ui.R.raw.urgentalarm to "urgentalarm" + private val SOUND_NAMES: Map = mapOf( + AlarmSound.ALARM to "alarm", + AlarmSound.BOLUS_ERROR to "boluserror", + AlarmSound.ERROR to "error", + AlarmSound.URGENT_ALARM to "urgentalarm" ) - private val DISPLAY_NAMES: Map = mapOf( - app.aaps.core.ui.R.raw.alarm to "Standard alarm", - app.aaps.core.ui.R.raw.boluserror to "Bolus error", - app.aaps.core.ui.R.raw.error to "General error", - app.aaps.core.ui.R.raw.urgentalarm to "Urgent alarm" + private val DISPLAY_NAMES: Map = mapOf( + AlarmSound.ALARM to "Standard alarm", + AlarmSound.BOLUS_ERROR to "Bolus error", + AlarmSound.ERROR to "General error", + AlarmSound.URGENT_ALARM to "Urgent alarm" ) } @@ -170,8 +171,8 @@ class AlarmNotificationManager @Inject constructor( // tap the notification. When the FSI does auto-launch (lockscreen/idle), ErrorActivity's // looping ramped audio takes over — its volume ramp starts at 0 so the overlap with the // channel one-shot is inaudible. - for ((soundId, displayName) in DISPLAY_NAMES) { - val uri: Uri = Uri.parse("android.resource://${context.packageName}/$soundId") + for ((sound, displayName) in DISPLAY_NAMES) { + val uri: Uri = Uri.parse("android.resource://${context.packageName}/${sound.rawRes}") val alarmAttrs = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) @@ -179,7 +180,7 @@ class AlarmNotificationManager @Inject constructor( .build() mgr.createNotificationChannel( NotificationChannel( - channelIdForSound(soundId, overrideDnd = true), + channelIdForSound(sound, overrideDnd = true), "$displayName (override DND)", NotificationManager.IMPORTANCE_HIGH ).apply { @@ -197,7 +198,7 @@ class AlarmNotificationManager @Inject constructor( // For medical alarms we always want heads-up visibility. mgr.createNotificationChannel( NotificationChannel( - channelIdForSound(soundId, overrideDnd = false), + channelIdForSound(sound, overrideDnd = false), "$displayName (respects DND)", NotificationManager.IMPORTANCE_HIGH ).apply { @@ -217,8 +218,8 @@ class AlarmNotificationManager @Inject constructor( } } - private fun channelIdForSound(@RawRes soundId: Int, overrideDnd: Boolean): String { - val name = SOUND_NAMES[soundId] ?: "error" + private fun channelIdForSound(sound: AlarmSound, overrideDnd: Boolean): String { + val name = SOUND_NAMES[sound] ?: "error" val suffix = if (overrideDnd) "alarm" else "notify" return "aaps_alarm_${name}_$suffix" } @@ -229,7 +230,7 @@ class AlarmNotificationManager @Inject constructor( * launches it full-screen (device idle / lock screen) or shows a heads-up notification. * The activity is responsible for sound playback. */ - fun postFullScreenAlarm(status: String, title: String, @RawRes soundId: Int) { + fun postFullScreenAlarm(status: String, title: String, sound: AlarmSound?) { // Reached only from the background / off-main branches of UiInteraction.runAlarm. // // Screen-wake + full-screen ErrorActivity launch no longer rely on USE_FULL_SCREEN_INTENT @@ -249,7 +250,7 @@ class AlarmNotificationManager @Inject constructor( // player treats as idempotent (no restart glitch). val postedAt = SystemClock.elapsedRealtime() val intent = Intent(context, uiInteractionProvider.get().errorHelperActivity).apply { - putExtra(AlarmIntent.EXTRA_SOUND_ID, soundId) + putExtra(AlarmIntent.EXTRA_SOUND, sound?.name) putExtra(AlarmIntent.EXTRA_STATUS, status) putExtra(AlarmIntent.EXTRA_TITLE, title) putExtra(AlarmIntent.EXTRA_POSTED_AT_ELAPSED_REALTIME, postedAt) @@ -263,9 +264,9 @@ class AlarmNotificationManager @Inject constructor( ) val overrideDnd = preferences.get(BooleanKey.AlertOverrideDoNotDisturb) - // Always a sound-bearing channel (matching the requested soundId + DND preference) so the alarm + // Always a sound-bearing channel (matching the requested sound + DND preference) so the alarm // is audible from the notification regardless of whether the FSI activity ever launches. - val channelId = channelIdForSound(soundId, overrideDnd) + val channelId = channelIdForSound(sound ?: AlarmSound.ERROR, overrideDnd) // Mute action so the user can silence the looping alarm straight from the lock-screen // notification when ErrorActivity isn't in the foreground (see AlarmMuteReceiver). @@ -305,7 +306,7 @@ class AlarmNotificationManager @Inject constructor( // Continuous looping/ramping audio so the alarm keeps sounding even if ErrorActivity never // comes to the foreground (deferred past the channel one-shot via postedAt). Stopped by // muteAllAlarms() / ErrorActivity acknowledge, both of which stop OWNER_FULLSCREEN. - alarmSoundPlayer.play(soundId, AlarmSoundPlayer.OWNER_FULLSCREEN, postedAt) + sound?.let { alarmSoundPlayer.play(it, AlarmSoundPlayer.OWNER_FULLSCREEN, postedAt) } // Permission-free screen-wake + activity launch (independent of the notification above, so it // still runs even if POST_NOTIFICATIONS was revoked and mgr.notify threw). diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt index 0c7b367375e9..cc2238f10d33 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt @@ -9,6 +9,8 @@ import android.os.Handler import android.os.Looper import android.os.SystemClock import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound +import app.aaps.core.ui.rawRes import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.AlarmSoundPlayer @@ -42,31 +44,30 @@ class AlarmSoundPlayerImpl @Inject constructor( // --- main-looper-confined state --- private var player: MediaPlayer? = null - @RawRes private var currentSound: Int = 0 + private var currentSound: AlarmSound? = null private var currentOwner: String? = null private var currentVolumeLevel = 0 // Stable reference so a deferred start can be cancelled by doStop() during the channel-sound guard. private val startRunnable = Runnable { startMediaPlayer() } - override fun play(@RawRes soundRes: Int, ownerTag: String, postedAtElapsedRealtime: Long) { - if (soundRes == 0) return - handler.post { doPlay(soundRes, ownerTag, postedAtElapsedRealtime) } + override fun play(sound: AlarmSound, ownerTag: String, postedAtElapsedRealtime: Long) { + handler.post { doPlay(sound, ownerTag, postedAtElapsedRealtime) } } override fun stop(ownerTag: String) { handler.post { if (currentOwner == ownerTag) doStop() } } - private fun doPlay(@RawRes soundRes: Int, ownerTag: String, postedAtElapsedRealtime: Long) { + private fun doPlay(sound: AlarmSound, ownerTag: String, postedAtElapsedRealtime: Long) { // Idempotent: a re-request of the exact same sound by the same owner while it's already // playing is a no-op. This lets ErrorActivity call play() when it foregrounds on top of an // alarm that AlarmNotificationManager already started (same OWNER_FULLSCREEN) without a // stop/restart audio glitch. - if (player != null && currentOwner == ownerTag && currentSound == soundRes) return + if (player != null && currentOwner == ownerTag && currentSound == sound) return doStop() - currentSound = soundRes + currentSound = sound currentOwner = ownerTag // Only the full-screen path passes postedAt > 0 (it has an accompanying channel one-shot to @@ -74,7 +75,7 @@ class AlarmSoundPlayerImpl @Inject constructor( // probe entirely and start immediately. val deferralMs = if (postedAtElapsedRealtime > 0L) { - val soundDurationMs = probeSoundDurationMs(soundRes) + val soundDurationMs = probeSoundDurationMs(sound.rawRes) (soundDurationMs - (SystemClock.elapsedRealtime() - postedAtElapsedRealtime)).coerceAtLeast(0L) } else 0L @@ -102,8 +103,7 @@ class AlarmSoundPlayerImpl @Inject constructor( } private fun startMediaPlayer() { - val soundRes = currentSound - if (soundRes == 0) return + val soundRes = currentSound?.rawRes ?: return val overrideDnd = preferences.get(BooleanKey.AlertOverrideDoNotDisturb) val audioAttrs = AudioAttributes.Builder() diff --git a/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt index 80a93ef70d0f..3c992f09860a 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt @@ -27,6 +27,7 @@ import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.UserEntryLogger +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.ActivePlugin @@ -1248,7 +1249,7 @@ class WizardBolusExecutorImpl @Inject constructor( // SMB stays silent (the loop self-corrects next cycle). onError still fires so the initiating // transport relays too (the watch's sendError today; a client's late ack in phase 1b). if (detailedBolusInfo.bolusType != BS.Type.SMB) - notificationManager.post(NotificationId.BOLUS_DELIVERY_FAILED, errorText, validMinutes = 0, soundRes = R.raw.boluserror) + notificationManager.post(NotificationId.BOLUS_DELIVERY_FAILED, errorText, validMinutes = 0, sound = AlarmSound.BOLUS_ERROR) onError(errorText) } else onSuccess() diff --git a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt index a845bec0e4c9..d209cac1335c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt @@ -10,7 +10,7 @@ import android.content.IntentFilter import android.media.AudioManager import android.media.RingtoneManager import android.os.Build -import androidx.annotation.RawRes +import app.aaps.core.interfaces.notifications.AlarmSound import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import app.aaps.core.interfaces.logging.AAPSLogger @@ -108,7 +108,7 @@ class NotificationManagerImpl @Inject constructor( text: String, level: NotificationLevel, validMinutes: Int, - @RawRes soundRes: Int?, + sound: AlarmSound?, actions: List, validityCheck: (() -> Boolean)? ): NotificationHandle { @@ -117,7 +117,7 @@ class NotificationManagerImpl @Inject constructor( return postInternal( id = id, text = text, level = level, date = now, validTo = validTo, - soundRes = soundRes, actions = actions, validityCheck = validityCheck + sound = sound, actions = actions, validityCheck = validityCheck ) } @@ -128,14 +128,14 @@ class NotificationManagerImpl @Inject constructor( level: NotificationLevel, date: Long, validTo: Long, - @RawRes soundRes: Int?, + sound: AlarmSound?, actions: List, validityCheck: (() -> Boolean)? ): NotificationHandle { return postInternal( id = id, text = text, level = level, date = date, validTo = validTo, - soundRes = soundRes, actions = actions, validityCheck = validityCheck + sound = sound, actions = actions, validityCheck = validityCheck ) } @@ -145,7 +145,7 @@ class NotificationManagerImpl @Inject constructor( level: NotificationLevel, date: Long, validTo: Long, - @RawRes soundRes: Int?, + sound: AlarmSound?, actions: List, validityCheck: (() -> Boolean)? ): NotificationHandle { @@ -173,7 +173,7 @@ class NotificationManagerImpl @Inject constructor( level = level, date = date, validTo = validTo, - soundRes = soundRes, + sound = sound, actions = actions, validityCheck = validityCheck ) @@ -185,8 +185,8 @@ class NotificationManagerImpl @Inject constructor( // Alarm tier (URGENT + sound): the system notification is silent (heads-up + vibration, no // channel sound); the ramping audio is owned by AlarmSoundPlayer and driven by // refreshAlarmSound() below so concurrent URGENT alarms hand off correctly. Sound is gated - // on URGENT — a soundRes on a lower level is intentionally ignored (only the alarm tier rings). - if (level == NotificationLevel.URGENT && soundRes != null && soundRes != 0) { + // on URGENT — a sound on a lower level is intentionally ignored (only the alarm tier rings). + if (level == NotificationLevel.URGENT && sound != null) { alarmNotificationManager.postSilentAlarmNotification( notificationKey = instanceKey, title = rh.gs(app.aaps.core.ui.R.string.urgent_alarm), @@ -212,7 +212,7 @@ class NotificationManagerImpl @Inject constructor( validMinutes: Int, date: Long, validTo: Long, - @RawRes soundRes: Int?, + sound: AlarmSound?, actions: List, validityCheck: (() -> Boolean)? ): NotificationHandle { @@ -221,7 +221,7 @@ class NotificationManagerImpl @Inject constructor( return postInternal( id = id, text = text, level = level, date = date, validTo = effectiveValidTo, - soundRes = soundRes, actions = actions, validityCheck = validityCheck + sound = sound, actions = actions, validityCheck = validityCheck ) } @@ -268,7 +268,7 @@ class NotificationManagerImpl @Inject constructor( @Synchronized override fun muteAllAlarms() { val current = _notifications.value - val audible = current.filter { it.level == NotificationLevel.URGENT && it.soundRes != null && it.soundRes != 0 } + val audible = current.filter { it.level == NotificationLevel.URGENT && it.sound != null } if (audible.isNotEmpty()) { audible.forEach { cancelSilentAlarmNotification(it) } _notifications.value = current - audible.toSet() @@ -307,7 +307,7 @@ class NotificationManagerImpl @Inject constructor( * audio is (re)evaluated separately by [refreshAlarmSound] after the registry has changed. */ private fun cancelSilentAlarmNotification(n: AapsNotification) { - if (n.soundRes != null) alarmNotificationManager.cancelSoundAlarm(n.instanceKey) + if (n.sound != null) alarmNotificationManager.cancelSoundAlarm(n.instanceKey) } /** @@ -321,7 +321,7 @@ class NotificationManagerImpl @Inject constructor( */ private fun refreshAlarmSound() { val top = _notifications.value - .filter { it.level == NotificationLevel.URGENT && it.soundRes != null && it.soundRes != 0 } + .filter { it.level == NotificationLevel.URGENT && it.sound != null } .maxByOrNull { it.date } when { top == null -> @@ -332,7 +332,7 @@ class NotificationManagerImpl @Inject constructor( top.instanceKey != soundingKey -> { soundingKey = top.instanceKey - alarmSoundPlayer.play(top.soundRes!!, AlarmSoundPlayer.OWNER_INTERNAL) + alarmSoundPlayer.play(top.sound!!, AlarmSoundPlayer.OWNER_INTERNAL) } // else: already playing the top alarm — leave the ramp running. } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index ce86fb5724f8..fd31e45f4926 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -18,6 +18,7 @@ import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.ActivePlugin @@ -192,7 +193,7 @@ class CommandQueueImplementation @Inject constructor( * [PumpEnactResult]; none post profile-set notifications themselves). `internal` so it can be unit-tested. * * - failure (timeout `result == null`, or `!success`): post the persistent [NotificationId.FAILED_UPDATE_PROFILE] - * "wrong basal until fixed" card, rung via [app.aaps.core.ui.R.raw.boluserror]; the driver's `comment` supplies + * "wrong basal until fixed" card, rung via [AlarmSound.BOLUS_ERROR]; the driver's `comment` supplies * the reason (a timeout has none). Deliberately NOT a full-screen `runAlarm` — a wrong base profile is serious * but persistent, so a dismissible alarm-notification is the right weight (a failed TBR is even quieter, * surfaced only in the loop status). @@ -211,7 +212,7 @@ class CommandQueueImplementation @Inject constructor( notificationManager.post( NotificationId.FAILED_UPDATE_PROFILE, result?.comment?.takeIf { it.isNotBlank() } ?: rh.gs(app.aaps.core.ui.R.string.failed_update_basal_profile), - soundRes = app.aaps.core.ui.R.raw.boluserror + sound = AlarmSound.BOLUS_ERROR ) return false } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/utils/HardLimitsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/utils/HardLimitsImpl.kt index d6f8e5ef40e0..084a1e60071c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/utils/HardLimitsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/utils/HardLimitsImpl.kt @@ -7,6 +7,7 @@ import app.aaps.core.data.ue.ValueWithUnit import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.resources.ResourceHelper @@ -77,7 +78,7 @@ class HardLimitsImpl @Inject constructor( ) } rxBus.send(EventShowSnackbar(msg, EventShowSnackbar.Type.Warning)) - notificationManager.post(NotificationId.TOAST_ALARM, msg, soundRes = app.aaps.core.ui.R.raw.error) + notificationManager.post(NotificationId.TOAST_ALARM, msg, sound = AlarmSound.ERROR) } return newValue } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImplTest.kt index 10d02c96c3e0..b2a06c0a47c8 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImplTest.kt @@ -22,6 +22,7 @@ import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.UserEntryLogger import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.profile.EffectiveProfile @@ -895,7 +896,7 @@ class WizardBolusExecutorImplTest : TestBaseWithProfile() { // The async delivery failure raises the single URGENT alarm from the executor (not the now-gone dialog). verify(notificationManager).post( eq(NotificationId.BOLUS_DELIVERY_FAILED), any(), any(), any(), - anyOrNull(), any>(), anyOrNull<() -> Boolean>() + anyOrNull(), any>(), anyOrNull<() -> Boolean>() ) } @@ -914,7 +915,7 @@ class WizardBolusExecutorImplTest : TestBaseWithProfile() { // A user-initiated cancel is not a failure: no URGENT alarm even though the command result is unsuccessful. verify(notificationManager, never()).post( eq(NotificationId.BOLUS_DELIVERY_FAILED), any(), any(), any(), - anyOrNull(), any>(), anyOrNull<() -> Boolean>() + anyOrNull(), any>(), anyOrNull<() -> Boolean>() ) } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index a1df5c88efcf..441ea11e45e3 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -10,6 +10,7 @@ import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -262,7 +263,7 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { return result } - // Both helpers match the String post() overload (id, text, level, validMinutes, soundRes, actions, validityCheck). + // Both helpers match the String post() overload (id, text, level, validMinutes, sound, actions, validityCheck). private fun verifyOkPosted(text: String) = verify(notificationManager).post( eq(NotificationId.PROFILE_SET_OK), eq(text), any(), any(), @@ -272,7 +273,7 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { private fun verifyFailurePosted(text: String) = verify(notificationManager).post( eq(NotificationId.FAILED_UPDATE_PROFILE), eq(text), any(), any(), - eq(app.aaps.core.ui.R.raw.boluserror), any>(), anyOrNull() + eq(AlarmSound.BOLUS_ERROR), any>(), anyOrNull() ) private fun verifyNothingPosted() = diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/TimerReminderReceiver.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/TimerReminderReceiver.kt index 9a8af43c372f..dc7e183768ec 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/TimerReminderReceiver.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/TimerReminderReceiver.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.automation import android.content.Context import android.content.Intent +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag @@ -32,7 +33,7 @@ class TimerReminderReceiver : DaggerBroadcastReceiver() { super.onReceive(context, intent) val text = intent.getStringExtra(EXTRA_TEXT)?.takeIf { it.isNotBlank() } ?: rh.gs(config.appName) aapsLogger.debug(LTag.AUTOMATION, "TimerReminderReceiver fired: $text") - uiInteraction.runAlarm(status = text, title = rh.gs(config.appName), soundId = CoreUiR.raw.alarm) + uiInteraction.runAlarm(status = text, title = rh.gs(config.appName), sound = AlarmSound.ALARM) } companion object { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt index d5b92bc7ca43..a4a03b386a30 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt @@ -11,6 +11,7 @@ import app.aaps.core.interfaces.insulin.InsulinType import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.nsclient.NSClientRepository @@ -215,7 +216,7 @@ class NsIncomingDataProcessor @Inject constructor( id = NotificationId.NS_ANNOUNCEMENT, text = therapyEvent.note ?: "", validTo = dateUtil.now() + T.mins(60).msecs(), - soundRes = R.raw.alarm, + sound = AlarmSound.ALARM, actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.snooze)) { }) ) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt index e14da6fffca8..8d1d89212d19 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTrip.kt @@ -10,6 +10,7 @@ import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.nsclient.NSClientRepository @@ -180,7 +181,7 @@ class ClientControlRoundTrip @Inject constructor( NotificationId.BOLUS_DELIVERY_FAILED, // payload is the master-authored full text ("title\n"); show it as-is, don't re-prefix the title. ack.payload ?: rh.gs(app.aaps.core.ui.R.string.treatmentdeliveryerror), - validMinutes = 0, soundRes = app.aaps.core.ui.R.raw.boluserror + validMinutes = 0, sound = AlarmSound.BOLUS_ERROR ) return } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index deaf542e4263..980f4944657a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -470,14 +471,14 @@ class NSClientV3Service : DaggerService() { 1 -> notificationManager.post( id = NotificationId.NS_ALARM, text = nsAlarm.title, - soundRes = app.aaps.core.ui.R.raw.alarm, + sound = AlarmSound.ALARM, actions = snoozeActions(nsAlarm) ) 2 -> notificationManager.post( id = NotificationId.NS_URGENT_ALARM, text = nsAlarm.title, - soundRes = app.aaps.core.ui.R.raw.urgentalarm, + sound = AlarmSound.URGENT_ALARM, actions = snoozeActions(nsAlarm) ) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt index ffd47af4267b..bd0e42fabce3 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/ClientControlRoundTripTest.kt @@ -7,6 +7,7 @@ import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.notifications.AapsNotification import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -119,7 +120,7 @@ internal class ClientControlRoundTripTest { verify(notificationManager).post( eq(NotificationId.BOLUS_DELIVERY_FAILED), any(), any(), any(), - anyOrNull(), any>(), anyOrNull<() -> Boolean>() + anyOrNull(), any>(), anyOrNull<() -> Boolean>() ) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt index aca54f2f61a9..8990a981ba33 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt @@ -7,6 +7,7 @@ import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.time.T.Companion.mins import app.aaps.core.data.time.T.Companion.secs import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.DetailedBolusInfo @@ -120,7 +121,7 @@ class DanaRv2ExecutionService : AbstractDanaRExecutionService() { if (abs(timeDiff) > 60 * 60 * 1.5) { aapsLogger.debug(LTag.PUMP, "Pump time difference: $timeDiff seconds - large difference") //If time-diff is very large, warn user until we can synchronize history readings properly - uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), app.aaps.core.ui.R.raw.error) + uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), AlarmSound.ERROR) //de-initialize pump danaPump.reset() diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt index d3fada360828..76ff917b8edb 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt @@ -10,6 +10,7 @@ import app.aaps.core.data.time.T import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.ActivePlugin @@ -244,7 +245,7 @@ class DanaRSService : DaggerService() { if (abs(timeDiff) > 60 * 60 * 1.5) { aapsLogger.debug(LTag.PUMPCOMM, "Pump time difference: $timeDiff seconds - large difference") //If time-diff is very large, warn user until we can synchronize history readings properly - uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), app.aaps.core.ui.R.raw.error) + uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), AlarmSound.ERROR) //de-initialize pump danaPump.reset() diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/BLECommonService.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/BLECommonService.kt index ab04614bf363..6e130d237edc 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/BLECommonService.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/BLECommonService.kt @@ -15,6 +15,7 @@ import android.content.Context import android.content.pm.PackageManager import android.os.SystemClock import androidx.core.app.ActivityCompat +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.resources.ResourceHelper @@ -353,20 +354,20 @@ class BLECommonService @Inject internal constructor( if (message is InjectionBlockReportPacket) { message.handleMessage(data) diaconnG8Pump.bolusBlocked = true - uiInteraction.runAlarm(rh.gs(R.string.injectionblocked), rh.gs(R.string.injectionblocked), app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(rh.gs(R.string.injectionblocked), rh.gs(R.string.injectionblocked), AlarmSound.BOLUS_ERROR) return } // battery warning report if (message is BatteryWarningReportPacket) { message.handleMessage(data) - uiInteraction.runAlarm(rh.gs(R.string.needbatteryreplace), rh.gs(R.string.batterywarning), app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(rh.gs(R.string.needbatteryreplace), rh.gs(R.string.batterywarning), AlarmSound.BOLUS_ERROR) return } // insulin lack warning report if (message is InsulinLackReportPacket) { message.handleMessage(data) - uiInteraction.runAlarm(rh.gs(R.string.needinsullinreplace), rh.gs(R.string.insulinlackwarning), app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(rh.gs(R.string.needinsullinreplace), rh.gs(R.string.insulinlackwarning), AlarmSound.BOLUS_ERROR) return } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt index f15679bce7f3..a78c13fe7fe9 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt @@ -11,6 +11,7 @@ import app.aaps.core.data.time.T import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.ActivePlugin @@ -242,7 +243,7 @@ class DiaconnG8Service : DaggerService() { if (abs(timeDiff) > 60 * 60 * 1.5) { aapsLogger.debug(LTag.PUMPCOMM, "Pump time difference: $timeDiff seconds - large difference") //If time-diff is very large, warn user until we can synchronize history readings properly - uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), app.aaps.core.ui.R.raw.error) + uiInteraction.runAlarm(rh.gs(R.string.largetimediff), rh.gs(R.string.largetimedifftitle), AlarmSound.ERROR) //de-initialize pump diaconnG8Pump.reset() diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt index 30d83349230b..29b7b5de4015 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/alarm/AlarmManager.kt @@ -4,6 +4,7 @@ import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationAction +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -145,7 +146,7 @@ class AlarmManager @Inject constructor() : IAlarmManager { // Critical alarms trigger the global alarm sound overlay if (isCritical) { - uiInteraction.runAlarm(alarmMsg, resourceHelper.gs(app.aaps.core.ui.R.string.alarm), app.aaps.core.ui.R.raw.error) + uiInteraction.runAlarm(alarmMsg, resourceHelper.gs(app.aaps.core.ui.R.string.alarm), AlarmSound.ERROR) } notificationManager.post( @@ -153,7 +154,7 @@ class AlarmManager @Inject constructor() : IAlarmManager { text = alarmMsg, level = if (isCritical) NotificationLevel.IMPORTANT else NotificationLevel.INFO, date = alarms.getOccuredAlarmTimestamp(alarmCode), - soundRes = if (!isCritical) app.aaps.core.ui.R.raw.error else null, + sound = if (!isCritical) AlarmSound.ERROR else null, actions = listOf( NotificationAction( TextRef.AndroidRes( diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt index 9e5ed4152cb2..fda6d8091426 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt @@ -4,6 +4,7 @@ import android.content.Context import app.aaps.core.data.model.TE import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -109,7 +110,7 @@ class PatchManager @Inject constructor( id = NotificationId.EOFLOW_PATCH_ALERT, text = rh.gs(R.string.patch_activate_reminder_desc), level = NotificationLevel.URGENT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) }) ) diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index f1b4aa382243..f50b10a9db8c 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -134,7 +135,7 @@ class EquilPumpPlugin @Inject constructor( // (alarms now come from the GATT history read on every connection, not just from an // advertisement scan caught mid-bolus). See #5040. notificationManager.dismiss(NotificationId.EQUIL_ALARM) - notificationManager.post(NotificationId.EQUIL_ALARM, eventEquilError.tips, soundRes = app.aaps.core.ui.R.raw.alarm) + notificationManager.post(NotificationId.EQUIL_ALARM, eventEquilError.tips, sound = AlarmSound.ALARM) // But only halt bolus tracking if a bolus is actually delivering. if (commandQueue.performing()?.commandType == Command.CommandType.BOLUS) { stopBolusDelivering() @@ -416,7 +417,7 @@ class EquilPumpPlugin @Inject constructor( notificationManager.post( NotificationId.EQUIL_LOW_BATTERY, rh.gs(R.string.equil_low_battery) + battery + "%", - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) preferences.put(EquilBooleanKey.AlarmBattery10, true) } else { @@ -425,7 +426,7 @@ class EquilPumpPlugin @Inject constructor( NotificationId.EQUIL_LOW_BATTERY, rh.gs(R.string.equil_low_battery) + battery + "%", NotificationLevel.IMPORTANT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) } } @@ -440,7 +441,7 @@ class EquilPumpPlugin @Inject constructor( notificationManager.post( NotificationId.EQUIL_ALARM_INSULIN, rh.gs(R.string.equil_low_insulin) + insulin + "U", - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) preferences.put(EquilBooleanKey.AlarmInsulin10, true) } @@ -453,7 +454,7 @@ class EquilPumpPlugin @Inject constructor( notificationManager.post( NotificationId.EQUIL_ALARM_INSULIN, rh.gs(R.string.equil_low_insulin) + insulin + "U", - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) preferences.put(EquilBooleanKey.AlarmInsulin5, true) } @@ -464,7 +465,7 @@ class EquilPumpPlugin @Inject constructor( notificationManager.post( NotificationId.EQUIL_ALARM_INSULIN, rh.gs(R.string.equil_low_insulin) + insulin + "U", - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) } } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt index f7880f0d9a4c..914bb37996d8 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/manager/EquilManager.kt @@ -6,6 +6,7 @@ import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.pump.BolusProgressData @@ -734,7 +735,7 @@ class EquilManager @Inject constructor( val parm = data[27].toInt() and 0xff val errorTips = getEquilError(port, level, parm) if (!TextUtils.isEmpty(errorTips) && currentIndex != historyIndex) { - notificationManager.post(NotificationId.PUMP_ERROR, errorTips, soundRes = app.aaps.core.ui.R.raw.alarm) + notificationManager.post(NotificationId.PUMP_ERROR, errorTips, sound = AlarmSound.ALARM) if (saveData) { val time = System.currentTimeMillis() val equilHistoryRecord = EquilHistoryRecord(EquilHistoryRecord.EventType.EQUIL_ALARM, time, getSerialNumber()) diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index 42c2b8c85688..204d903af888 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -12,6 +12,7 @@ import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.pump.defs.TimeChangeType import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.PluginDescription @@ -674,7 +675,7 @@ class MedtronicPumpPlugin @Inject constructor( // LOG.debug("MedtronicPumpPlugin::deliverBolus - Delivery Canceled after Bolus started."); Thread { SystemClock.sleep(2000) - uiInteraction.runAlarm(rh.gs(R.string.medtronic_cmd_cancel_bolus_not_supported), rh.gs(app.aaps.core.ui.R.string.warning), app.aaps.core.ui.R.raw.boluserror) + uiInteraction.runAlarm(rh.gs(R.string.medtronic_cmd_cancel_bolus_not_supported), rh.gs(app.aaps.core.ui.R.string.warning), AlarmSound.BOLUS_ERROR) }.start() } val now = System.currentTimeMillis() diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt index 97e2c21cb702..7b16660ea795 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt @@ -10,6 +10,7 @@ import app.aaps.core.data.time.T import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -617,7 +618,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { NotificationId.PUMP_SYNC_ERROR, R.string.pump_sync_error, level = NotificationLevel.URGENT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) } else if (failureCount >= 2) { break @@ -705,7 +706,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { NotificationId.PUMP_ERROR, R.string.patch_reset_after_primed_error, level = NotificationLevel.URGENT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) } } @@ -745,7 +746,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { NotificationId.PUMP_SUSPENDED, R.string.pump_is_suspended_hour_max, level = NotificationLevel.URGENT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) // Pump will report proper TBR for this from loadEvents() scope.launch { commandQueue.loadEvents() } @@ -756,7 +757,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { NotificationId.PUMP_SUSPENDED, R.string.pump_is_suspended_day_max, level = NotificationLevel.URGENT, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) // Pump will report proper TBR for this from loadEvents() scope.launch { commandQueue.loadEvents() } @@ -776,7 +777,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { notificationManager.post( NotificationId.PUMP_ERROR, R.string.pump_error, alarmState?.let { medtrumPump.alarmStateToString(it) }, - soundRes = app.aaps.core.ui.R.raw.alarm + sound = AlarmSound.ALARM ) // Get pump status, use readStatus here as for loadEvents() we cannot be sure callback is executed scope.launch { diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 08d535ca1b52..9d465eb92998 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -9,6 +9,7 @@ import app.aaps.core.data.pump.defs.TimeChangeType import app.aaps.core.data.time.T import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager @@ -211,7 +212,7 @@ class OmnipodDashPumpPlugin @Inject constructor( showNotification( NotificationId.OMNIPOD_POD_SUSPENDED, rh.gs(R.string.insulin_delivery_suspended), - app.aaps.core.ui.R.raw.boluserror + AlarmSound.BOLUS_ERROR ) } else { notificationManager.dismiss(NotificationId.OMNIPOD_POD_SUSPENDED) @@ -389,7 +390,7 @@ class OmnipodDashPumpPlugin @Inject constructor( showNotification( NotificationId.OMNIPOD_POD_FAULT, it.toString(), - app.aaps.core.ui.R.raw.boluserror + AlarmSound.BOLUS_ERROR ) } pumpSync.insertAnnouncement( @@ -473,7 +474,7 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.FAILED_UPDATE_PROFILE, rh.gs(R.string.suspend_delivery_is_unconfirmed), - app.aaps.core.ui.R.raw.boluserror, + AlarmSound.BOLUS_ERROR, level = NotificationLevel.IMPORTANT ) } @@ -702,11 +703,11 @@ class OmnipodDashPumpPlugin @Inject constructor( NotificationId.OMNIPOD_UNCERTAIN_SMB, "Unable to verify whether SMB bolus ($requestedBolusAmount U) succeeded. " + "Refresh pod status to confirm or deny this command.", - app.aaps.core.ui.R.raw.boluserror + AlarmSound.BOLUS_ERROR ) } else { if (podStateManager.activeCommand != null) { - val sound = if (hasBolusErrorBeepEnabled()) app.aaps.core.ui.R.raw.boluserror else 0 + val sound = if (hasBolusErrorBeepEnabled()) AlarmSound.BOLUS_ERROR else null showErrorDialog(rh.gs(R.string.bolus_delivery_status_uncertain), sound) } } @@ -911,7 +912,7 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.OMNIPOD_TBR_ALERTS, rh.gs(R.string.setting_temp_basal_might_have_basal_failed), - app.aaps.core.ui.R.raw.boluserror, + AlarmSound.BOLUS_ERROR, ) }.toPumpEnactResultImpl() @@ -969,7 +970,7 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.OMNIPOD_TBR_ALERTS, rh.gs(R.string.cancelling_temp_basal_might_have_failed), - app.aaps.core.ui.R.raw.boluserror, + AlarmSound.BOLUS_ERROR, ) } } @@ -1009,12 +1010,12 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.OMNIPOD_TBR_ALERTS, rh.gs(R.string.cancel_temp_basal_result_is_uncertain), - app.aaps.core.ui.R.raw.boluserror, // TODO: add setting for this + AlarmSound.BOLUS_ERROR, // TODO: add setting for this ) }.toPumpEnactResultImpl() } - private fun notifyOnUnconfirmed(notificationId: NotificationId, msg: String, sound: Int?, level: NotificationLevel = NotificationLevel.IMPORTANT) { + private fun notifyOnUnconfirmed(notificationId: NotificationId, msg: String, sound: AlarmSound?, level: NotificationLevel = NotificationLevel.IMPORTANT) { if (podStateManager.activeCommand != null) { aapsLogger.debug(LTag.PUMP, "Notification for active command: ${podStateManager.activeCommand}") showNotification(notificationId, msg, sound, level = level) @@ -1132,7 +1133,7 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.FAILED_UPDATE_PROFILE, rh.gs(R.string.unconfirmed_resumedelivery_command_please_refresh_pod_status), - app.aaps.core.ui.R.raw.boluserror, + AlarmSound.BOLUS_ERROR, level = NotificationLevel.IMPORTANT ) }.toPumpEnactResultImpl() @@ -1507,7 +1508,7 @@ class OmnipodDashPumpPlugin @Inject constructor( if (tbr != null && podStateManager.deliveryStatus?.basalActive() == true) { aapsLogger.error(LTag.PUMP, "AAPS expected a TBR running but pump has no TBR running! AAPS: ${expectedState.temporaryBasal} Pump: ${podStateManager.deliveryStatus}") // Alert user - val sound = if (hasBolusErrorBeepEnabled()) app.aaps.core.ui.R.raw.boluserror else 0 + val sound = if (hasBolusErrorBeepEnabled()) AlarmSound.BOLUS_ERROR else null showErrorDialog(rh.gs(R.string.temp_basal_out_of_sync), sound) // Sync stopped basal with AAPS val ret = pumpSync.syncStopTemporaryBasalWithPumpId( @@ -1521,7 +1522,7 @@ class OmnipodDashPumpPlugin @Inject constructor( } else if (tbr == null && podStateManager.deliveryStatus?.tempBasalActive() == true) { aapsLogger.error(LTag.PUMP, "AAPS expected no TBR running but pump has a TBR running! AAPS: ${expectedState.temporaryBasal} Pump: ${podStateManager.deliveryStatus}") // Alert user - val sound = if (hasBolusErrorBeepEnabled()) app.aaps.core.ui.R.raw.boluserror else 0 + val sound = if (hasBolusErrorBeepEnabled()) AlarmSound.BOLUS_ERROR else null showErrorDialog(rh.gs(R.string.temp_basal_out_of_sync), sound) // If this is reached is reached there is probably a something wrong with the time (maybe it has changed?). // No way to calculate the TBR end time and update pumpSync properly. @@ -1534,16 +1535,16 @@ class OmnipodDashPumpPlugin @Inject constructor( } } - private fun showErrorDialog(message: String, sound: Int) { + private fun showErrorDialog(message: String, sound: AlarmSound?) { uiInteraction.runAlarm(message, rh.gs(app.aaps.core.ui.R.string.error), sound) } - private fun showNotification(id: NotificationId, message: String, sound: Int?, level: NotificationLevel = id.defaultLevel) { + private fun showNotification(id: NotificationId, message: String, sound: AlarmSound?, level: NotificationLevel = id.defaultLevel) { notificationManager.post( id, message, level = level, - soundRes = if (sound != null && soundEnabledForNotificationType(id)) sound else null + sound = if (sound != null && soundEnabledForNotificationType(id)) sound else null ) } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index e1799a8dc5e1..4d6ff53e86f0 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -19,6 +19,7 @@ import app.aaps.core.data.time.T.Companion.msecs import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.OwnDatabasePlugin @@ -365,7 +366,7 @@ class OmnipodErosPumpPlugin @Inject constructor( } else { // Not sure what's going on. Notify the user aapsLogger.error(LTag.PUMP, "Unknown TBR in both Pod state and AAPS") - notificationManager.post(NotificationId.OMNIPOD_UNKNOWN_TBR, R.string.omnipod_eros_error_tbr_running_but_aaps_not_aware, soundRes = app.aaps.core.ui.R.raw.boluserror) + notificationManager.post(NotificationId.OMNIPOD_UNKNOWN_TBR, R.string.omnipod_eros_error_tbr_running_but_aaps_not_aware, sound = AlarmSound.BOLUS_ERROR) } } else if (!podStateManager.isTempBasalRunning && tempBasal != null) { aapsLogger.warn(LTag.PUMP, "Removing AAPS TBR that actually hadn't succeeded") @@ -659,7 +660,7 @@ class OmnipodErosPumpPlugin @Inject constructor( return pumpEnactResultProvider.get().success(false).enacted(false).comment(aapsOmnipodErosManager.translateException(ex)) } - uiInteraction.runAlarm(rh.gs(R.string.omnipod_eros_pod_management_pulse_log_value) + ":\n" + result.toString(), rh.gs(R.string.omnipod_eros_pod_management_pulse_log), 0) + uiInteraction.runAlarm(rh.gs(R.string.omnipod_eros_pod_management_pulse_log_value) + ":\n" + result.toString(), rh.gs(R.string.omnipod_eros_pod_management_pulse_log), null) return pumpEnactResultProvider.get().success(true).enacted(false) } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java index 46e63f3b4366..35cd5b82e78f 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java @@ -22,6 +22,7 @@ import app.aaps.core.interfaces.insulin.ConcentrationHelper; import app.aaps.core.interfaces.logging.AAPSLogger; import app.aaps.core.interfaces.logging.LTag; +import app.aaps.core.interfaces.notifications.AlarmSound; import app.aaps.core.interfaces.notifications.NotificationId; import app.aaps.core.interfaces.notifications.NotificationLevel; import app.aaps.core.interfaces.notifications.NotificationManager; @@ -412,9 +413,9 @@ public PumpEnactResult bolus(DetailedBolusInfo detailedBolusInfo) { if (OmnipodManager.CommandDeliveryStatus.UNCERTAIN_FAILURE.equals(bolusCommandResult.getCommandDeliveryStatus())) { // For safety reasons, we treat this as a bolus that has successfully been delivered, in order to prevent insulin overdose if (detailedBolusInfo.getBolusType() == BS.Type.SMB) { - showNotification(NotificationId.OMNIPOD_UNCERTAIN_SMB, getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain_smb, detailedBolusInfo.insulin), NotificationLevel.IMPORTANT, isNotificationUncertainSmbSoundEnabled() ? app.aaps.core.ui.R.raw.boluserror : null); + showNotification(NotificationId.OMNIPOD_UNCERTAIN_SMB, getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain_smb, detailedBolusInfo.insulin), NotificationLevel.IMPORTANT, isNotificationUncertainSmbSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); } else { - showErrorDialog(getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain), isNotificationUncertainBolusSoundEnabled() ? app.aaps.core.ui.R.raw.boluserror : 0); + showErrorDialog(getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain), isNotificationUncertainBolusSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); } } @@ -516,7 +517,7 @@ public PumpEnactResult cancelBolus() { String errorMessage = translateException(ex.getCause()); addFailureToHistory(PodHistoryEntryType.SET_TEMPORARY_BASAL, errorMessage); - showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_set_temp_basal_failed_old_tbr_might_be_cancelled), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? app.aaps.core.ui.R.raw.boluserror : null); + showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_set_temp_basal_failed_old_tbr_might_be_cancelled), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); splitActiveTbr(); // Split any active TBR so when we recover from the uncertain TBR status,we only cancel the part after the cancellation @@ -526,7 +527,7 @@ public PumpEnactResult cancelBolus() { long pumpId = addFailureToHistory(PodHistoryEntryType.SET_TEMPORARY_BASAL, errorMessage); if (!OmnipodManager.isCertainFailure(ex)) { - showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_set_temp_basal_failed_old_tbr_cancelled_new_might_have_failed), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? app.aaps.core.ui.R.raw.boluserror : null); + showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_set_temp_basal_failed_old_tbr_cancelled_new_might_have_failed), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); // Assume that setting the temp basal succeeded here, because in case it didn't succeed, // The next StatusResponse that we receive will allow us to recover from the wrong state @@ -558,7 +559,7 @@ public PumpEnactResult cancelTemporaryBasal() { executeCommand(() -> delegate.cancelTemporaryBasal(isTbrBeepsEnabled())); } catch (Exception ex) { if (OmnipodManager.isCertainFailure(ex)) { - showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_cancel_temp_basal_failed_uncertain), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? app.aaps.core.ui.R.raw.boluserror : null); + showNotification(NotificationId.OMNIPOD_TBR_ALERTS, getStringResource(R.string.omnipod_eros_error_cancel_temp_basal_failed_uncertain), NotificationLevel.IMPORTANT, isNotificationUncertainTbrSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); } else { splitActiveTbr(); // Split any active TBR so when we recover from the uncertain TBR status,we only cancel the part after the cancellation } @@ -623,21 +624,21 @@ public PumpEnactResult setTime(boolean showNotifications) { } catch (CommandFailedAfterChangingDeliveryStatusException ex) { createSuspendedFakeTbrIfNotExists(); if (showNotifications) { - showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_suspended), NotificationLevel.IMPORTANT, app.aaps.core.ui.R.raw.boluserror); + showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_suspended), NotificationLevel.IMPORTANT, AlarmSound.BOLUS_ERROR); } String errorMessage = translateException(ex.getCause()); addFailureToHistory(PodHistoryEntryType.SET_TIME, errorMessage); return pumpEnactResultProvider.get().success(false).enacted(false).comment(errorMessage); } catch (PrecedingCommandFailedUncertainlyException ex) { if (showNotifications) { - showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_might_be_suspended), NotificationLevel.IMPORTANT, app.aaps.core.ui.R.raw.boluserror); + showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_might_be_suspended), NotificationLevel.IMPORTANT, AlarmSound.BOLUS_ERROR); } String errorMessage = translateException(ex.getCause()); addFailureToHistory(PodHistoryEntryType.SET_TIME, errorMessage); return pumpEnactResultProvider.get().success(false).enacted(false).comment(errorMessage); } catch (Exception ex) { if (showNotifications) { - showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_might_be_suspended), NotificationLevel.IMPORTANT, app.aaps.core.ui.R.raw.boluserror); + showNotification(NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, getStringResource(R.string.omnipod_eros_error_set_time_failed_delivery_might_be_suspended), NotificationLevel.IMPORTANT, AlarmSound.BOLUS_ERROR); } String errorMessage = translateException(ex); addFailureToHistory(PodHistoryEntryType.SET_TIME, errorMessage); @@ -1020,19 +1021,19 @@ private void sendEvent(Event event) { rxBus.send(event); } - private void showErrorDialog(@NonNull String message, Integer sound) { + private void showErrorDialog(@NonNull String message, AlarmSound sound) { uiInteraction.runAlarm(message, rh.gs(app.aaps.core.ui.R.string.error), sound); } private void showPodFaultNotification(FaultEventCode faultEventCode) { - showPodFaultNotification(faultEventCode, app.aaps.core.ui.R.raw.boluserror); + showPodFaultNotification(faultEventCode, AlarmSound.BOLUS_ERROR); } - private void showPodFaultNotification(FaultEventCode faultEventCode, Integer sound) { + private void showPodFaultNotification(FaultEventCode faultEventCode, AlarmSound sound) { notificationManager.post(NotificationId.OMNIPOD_POD_FAULT, createPodFaultErrorMessage(faultEventCode), NotificationLevel.IMPORTANT, 0, sound, java.util.Collections.emptyList(), null); } - private void showNotification(NotificationId id, String message, NotificationLevel level, Integer sound) { + private void showNotification(NotificationId id, String message, NotificationLevel level, AlarmSound sound) { notificationManager.post(id, message, level, 0, sound, java.util.Collections.emptyList(), null); } diff --git a/ui/src/main/kotlin/app/aaps/ui/activities/ErrorActivity.kt b/ui/src/main/kotlin/app/aaps/ui/activities/ErrorActivity.kt index eae800a9a6af..6cbbb171b524 100644 --- a/ui/src/main/kotlin/app/aaps/ui/activities/ErrorActivity.kt +++ b/ui/src/main/kotlin/app/aaps/ui/activities/ErrorActivity.kt @@ -26,6 +26,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.UserEntryLogger import app.aaps.core.interfaces.notifications.AlarmIntent +import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.AlarmSoundPlayer import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.ui.IconsProvider @@ -63,7 +64,7 @@ class ErrorActivity : DaggerAppCompatActivity() { private var status by mutableStateOf("") private var title by mutableStateOf("") - private var sound: Int = 0 + private var sound: AlarmSound = AlarmSound.ERROR /** * `SystemClock.elapsedRealtime()` when the originating notification was posted (from @@ -86,7 +87,7 @@ class ErrorActivity : DaggerAppCompatActivity() { status = intent.getStringExtra(AlarmIntent.EXTRA_STATUS) ?: "" title = intent.getStringExtra(AlarmIntent.EXTRA_TITLE) ?: "" - sound = intent.getIntExtra(AlarmIntent.EXTRA_SOUND_ID, app.aaps.core.ui.R.raw.error) + sound = intent.getStringExtra(AlarmIntent.EXTRA_SOUND)?.let { runCatching { AlarmSound.valueOf(it) }.getOrNull() } ?: AlarmSound.ERROR postedAtElapsedRealtime = intent.getLongExtra(AlarmIntent.EXTRA_POSTED_AT_ELAPSED_REALTIME, 0L) val appIcon = iconsProvider.getIcon() @@ -143,7 +144,7 @@ class ErrorActivity : DaggerAppCompatActivity() { setIntent(intent) status = intent.getStringExtra(AlarmIntent.EXTRA_STATUS) ?: "" title = intent.getStringExtra(AlarmIntent.EXTRA_TITLE) ?: "" - sound = intent.getIntExtra(AlarmIntent.EXTRA_SOUND_ID, app.aaps.core.ui.R.raw.error) + sound = intent.getStringExtra(AlarmIntent.EXTRA_SOUND)?.let { runCatching { AlarmSound.valueOf(it) }.getOrNull() } ?: AlarmSound.ERROR postedAtElapsedRealtime = intent.getLongExtra(AlarmIntent.EXTRA_POSTED_AT_ELAPSED_REALTIME, 0L) aapsLogger.debug("Error activity updated: $title - $status") handler.removeCallbacksAndMessages(null) @@ -160,7 +161,7 @@ class ErrorActivity : DaggerAppCompatActivity() { } private fun startAlarm() { - if (sound != 0) alarmSoundPlayer.play(sound, AlarmSoundPlayer.OWNER_FULLSCREEN, postedAtElapsedRealtime) + alarmSoundPlayer.play(sound, AlarmSoundPlayer.OWNER_FULLSCREEN, postedAtElapsedRealtime) } private fun stopAlarm(reason: String) { From f08dda8056dd3ecaeab8d11debf09c42f93db247 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 20:54:46 +0200 Subject: [PATCH 068/146] ProfileStore and PairProfileStore on kotlinx JSON --- .../core/interfaces/profile/ProfileStore.kt | 8 +-- .../core/interfaces/sync/DataSyncSelector.kt | 4 +- .../profile/ProfileRepositoryImpl.kt | 64 +++++++++++-------- .../profile/ProfileStoreObject.kt | 29 +++++++-- .../profile/ProfileRepositoryImplTest.kt | 21 ++++++ .../profile/ProfileStoreObjectTest.kt | 8 ++- .../profile/ProfileStoreTest.kt | 2 +- .../plugins/aps/autotune/AutotunePlugin.kt | 3 +- .../aps/autotune/compose/AutotuneViewModel.kt | 9 +-- .../plugins/aps/autotune/data/ATProfile.kt | 4 +- .../sync/nsclientV3/DataSyncSelectorV3.kt | 7 +- .../sync/nsclientV3/NSClientV3Plugin.kt | 2 +- .../nsclientV3/NsIncomingDataProcessor.kt | 3 +- .../sync/xdrip/DataSyncSelectorXdripImpl.kt | 5 +- .../kotlin/app/aaps/pump/dana/DanaPump.kt | 4 +- .../aaps/shared/tests/TestBaseWithProfile.kt | 8 ++- 16 files changed, 119 insertions(+), 62 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt index 236df2b382fe..f0db70373db5 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt @@ -1,15 +1,15 @@ package app.aaps.core.interfaces.profile -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface ProfileStore { - fun with(data: JSONObject): ProfileStore + fun with(data: JsonObject): ProfileStore - fun getData(): JSONObject + fun getData(): JsonObject fun getStartDate(): Long fun getDefaultProfile(): PureProfile? - fun getDefaultProfileJson(): JSONObject? + fun getDefaultProfileJson(): JsonObject? fun getDefaultProfileName(): String? fun getProfileList(): ArrayList fun getSpecificProfile(profileName: String): PureProfile? diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt index 11c428c1a290..31c3ea702c27 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt @@ -14,7 +14,7 @@ import app.aaps.core.data.model.RM import app.aaps.core.data.model.TB import app.aaps.core.data.model.TE import app.aaps.core.data.model.TT -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface DataSyncSelector { @@ -38,7 +38,7 @@ interface DataSyncSelector { data class PairProfileSwitch(override val value: PS, override val id: Long, override var confirmed: Boolean = false) : DataPair data class PairEffectiveProfileSwitch(override val value: EPS, override val id: Long, override var confirmed: Boolean = false) : DataPair data class PairRunningMode(override val value: RM, override val id: Long, override var confirmed: Boolean = false) : DataPair - data class PairProfileStore(override val value: JSONObject, override val id: Long, override var confirmed: Boolean = false) : DataPair + data class PairProfileStore(override val value: JsonObject, override val id: Long, override var confirmed: Boolean = false) : DataPair data class PairDeviceStatus(override val value: DS, override val id: Long, override var confirmed: Boolean = false) : DataPair fun queueSize(): Long diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt index c28c8b1c0fae..e95c62b5d34d 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt @@ -31,11 +31,14 @@ import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.blockFromJsonArray import app.aaps.core.objects.extensions.highToJSONArray +import app.aaps.core.objects.extensions.highToJsonArray import app.aaps.core.objects.extensions.lowToJSONArray +import app.aaps.core.objects.extensions.lowToJsonArray import app.aaps.core.objects.extensions.singleBlock import app.aaps.core.objects.extensions.singleTargetBlock import app.aaps.core.objects.extensions.targetBlockFromJsonArray import app.aaps.core.objects.extensions.toJSONArray +import app.aaps.core.objects.extensions.toJsonArray import app.aaps.core.objects.extensions.toPureProfile import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.R @@ -50,6 +53,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -612,36 +618,40 @@ class ProfileRepositoryImpl @Inject constructor( ) } + /** + * Rebuild the by-name store that [profile] publishes. + * + * Built as kotlinx directly rather than as `org.json` and converted: [ProfileStore] takes a + * kotlinx tree now, and the block renderers already produce one, so the whole path from typed + * blocks to the published store stays in one representation. There is no `try/catch` any more + * because a builder cannot throw the way `JSONObject.put` could. + */ private fun createAndStoreConvertedProfile() { - val json = JSONObject() - val store = JSONObject() - try { - for (i in profilesList.indices) { - profilesList[i].run { - val pj = JSONObject() - pj.put("carbratio", ic.toJSONArray()) - pj.put("sens", isf.toJSONArray()) - pj.put("basal", basal.toJSONArray()) - pj.put("target_low", target.lowToJSONArray()) - pj.put("target_high", target.highToJSONArray()) - pj.put("units", if (mgdl) GlucoseUnit.MGDL.asText else GlucoseUnit.MMOL.asText) - pj.put("timezone", TimeZone.getDefault().id) - store.put(name, pj) + val startDate = preferences.getIfExists(LongNonKey.LocalProfileLastChange) ?: dateUtil.now() + rawProfile = profileStoreProvider.get().with( + buildJsonObject { + // First profile is the stable "default" for the serialised store. Activation + // decisions live in ProfileFunction (EPS-based); this field is purely cosmetic + // for NS upload and tooling that reads the store JSON directly. + if (profilesList.isNotEmpty()) put("defaultProfile", profilesList.first().name) + put("date", startDate) + put("created_at", dateUtil.toISOAsUTC(startDate)) + put("startDate", dateUtil.toISOAsUTC(startDate)) + putJsonObject("store") { + for (profile in profilesList) profile.run { + putJsonObject(name) { + put("carbratio", ic.toJsonArray()) + put("sens", isf.toJsonArray()) + put("basal", basal.toJsonArray()) + put("target_low", target.lowToJsonArray()) + put("target_high", target.highToJsonArray()) + put("units", if (mgdl) GlucoseUnit.MGDL.asText else GlucoseUnit.MMOL.asText) + put("timezone", TimeZone.getDefault().id) + } + } } } - // First profile is the stable "default" for the serialised store. Activation - // decisions live in ProfileFunction (EPS-based); this field is purely cosmetic - // for NS upload and tooling that reads the store JSON directly. - if (profilesList.isNotEmpty()) json.put("defaultProfile", profilesList.first().name) - val startDate = preferences.getIfExists(LongNonKey.LocalProfileLastChange) ?: dateUtil.now() - json.put("date", startDate) - json.put("created_at", dateUtil.toISOAsUTC(startDate)) - json.put("startDate", dateUtil.toISOAsUTC(startDate)) - json.put("store", store) - } catch (e: JSONException) { - aapsLogger.error("Unhandled exception", e) - } - rawProfile = profileStoreProvider.get().with(json) + ) } companion object { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileStoreObject.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileStoreObject.kt index 92d164b7d2ae..270c51360fd7 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileStoreObject.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileStoreObject.kt @@ -13,6 +13,9 @@ import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.objects.extensions.pureProfileFromJson import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.utils.JsonHelper +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import org.json.JSONException import org.json.JSONObject import javax.inject.Inject @@ -29,11 +32,27 @@ class ProfileStoreObject @Inject constructor( private lateinit var data: JSONObject - override fun with(data: JSONObject): ProfileStore = this.also { - this.data = data + /** + * Converts once at the boundary and keeps working on `org.json` inside. + * + * The interface speaks kotlinx so it can move to common code; the reading below still leans on + * `JsonHelper` and `pureProfileFromJson`, which are `org.json` throughout and are their own + * migration. This is the same inside-out step used for the profile block parsers: the contract + * crosses first, the internals follow. + */ + override fun with(data: JsonObject): ProfileStore = this.also { + this.data = JSONObject(data.toString()) } - override fun getData(): JSONObject = data + /** + * The store as kotlinx, rebuilt on each call. + * + * Deliberately not a view of [data]: the previous version handed out the live `JSONObject`, and + * both sync selectors wrote into it (`profileJson.put("date", …)`), mutating the store they had + * just read. A kotlinx tree is immutable, so that cannot happen - the callers now build the + * amended copy they actually wanted. + */ + override fun getData(): JsonObject = Json.parseToJsonElement(data.toString()).jsonObject private val cachedObjects = ArrayMap() @@ -58,8 +77,8 @@ class ProfileStoreObject @Inject constructor( } override fun getDefaultProfile(): PureProfile? = getDefaultProfileName()?.let { getSpecificProfile(it) } - override fun getDefaultProfileJson(): JSONObject? = - getDefaultProfileName()?.let { getSpecificProfileJson(it) } + override fun getDefaultProfileJson(): JsonObject? = + getDefaultProfileName()?.let { getSpecificProfileJson(it) }?.let { Json.parseToJsonElement(it.toString()).jsonObject } override fun getDefaultProfileName(): String? { val defaultProfileName = data.optString("defaultProfile") diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt index e7dcfefe4e2f..99072f29e5ce 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.Test @@ -363,6 +365,25 @@ class ProfileRepositoryImplTest : TestBaseWithProfile() { assertThat(sut.profiles.value).isSameInstanceAs(listBefore) } + /** + * The published store always carries a numeric `date`. + * + * Nightscout v3 needs that field, and both sync selectors now read it straight from the store. + * They used to patch it in when absent - a branch that could never fire, because this is the only + * producer and it writes `date` unconditionally. Deleting an unreachable guard is only safe if the + * invariant it guarded is pinned somewhere reachable, which is what this is. + */ + @Test + fun `the published store always carries a numeric date`() = runTest { + val sut = createSut() + assertThat(sut.profile.value?.getData()?.get("date")?.jsonPrimitive?.longOrNull).isNotNull() + + // And still after a mutation rebuilds it. + sut.add(profile("Mine")) + + assertThat(sut.profile.value?.getData()?.get("date")?.jsonPrimitive?.longOrNull).isNotNull() + } + /** The accepted case still bumps, otherwise the editor would never notice an NS push. */ @Test fun `an accepted Nightscout store does count as a mutation`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreObjectTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreObjectTest.kt index ec90d83f5711..a0bba7977bc2 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreObjectTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreObjectTest.kt @@ -9,7 +9,9 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.HardLimits import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.kotlin.whenever @@ -29,11 +31,11 @@ class ProfileStoreObjectTest : TestBase() { private fun store(json: String): ProfileStore = ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil) - .with(JSONObject(json)) + .with(Json.parseToJsonElement(json).jsonObject) @Test fun getData_returnsWrappedJson() { - assertThat(store("""{"defaultProfile":"D","store":{"D":{}}}""").getData().getString("defaultProfile")).isEqualTo("D") + assertThat(store("""{"defaultProfile":"D","store":{"D":{}}}""").getData()["defaultProfile"]?.jsonPrimitive?.content).isEqualTo("D") } @Test diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreTest.kt index c33f3113d569..74f70adb918a 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileStoreTest.kt @@ -22,7 +22,7 @@ internal class ProfileStoreTest : TestBaseWithProfile() { @Test fun getDefaultProfileJsonTest() { - assertThat(getValidProfileStore().getDefaultProfileJson()?.has("carbratio")).isTrue() + assertThat(getValidProfileStore().getDefaultProfileJson()?.containsKey("carbratio")).isTrue() assertThat(getInvalidProfileStore2().getDefaultProfileJson()).isNull() } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index c9b87f5df24d..7c94f77fb32b 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -44,6 +44,7 @@ import app.aaps.plugins.aps.autotune.data.ATProfile import app.aaps.plugins.aps.autotune.data.PreppedGlucose import app.aaps.plugins.aps.autotune.events.EventAutotuneUpdateGui import app.aaps.plugins.aps.autotune.keys.AutotuneStringKey +import kotlinx.serialization.json.JsonObject import org.json.JSONException import org.json.JSONObject import java.util.TimeZone @@ -398,7 +399,7 @@ class AutotunePlugin @Inject constructor( suspend fun updateProfile(newProfile: ATProfile?) { if (newProfile == null) return val circadian = preferences.get(BooleanKey.AutotuneCircadianIcIsf) - val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JSONObject()) + val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JsonObject(emptyMap())) val profileList: ArrayList = profileStore.getProfileList() var indexLocalProfile = -1 for (p in profileList.indices) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt index 35d508daa1fb..e612b894b520 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt @@ -34,6 +34,7 @@ import app.aaps.plugins.aps.autotune.AutotuneFS import app.aaps.plugins.aps.autotune.AutotunePlugin import app.aaps.plugins.aps.autotune.data.ATProfile import app.aaps.plugins.aps.autotune.events.EventAutotuneUpdateGui +import kotlinx.serialization.json.JsonObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -349,7 +350,7 @@ class AutotuneViewModel( fun onCheckInputProfile() { scope.launch { - val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JSONObject()) + val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JsonObject(emptyMap())) val pumpProfile = profileFunction.getProfile()?.let { currentProfile -> profileStore.getSpecificProfile(profileName)?.let { specificProfile -> atProfileProvider.get().with(ProfileSealed.Pure(specificProfile, null), currentProfile.iCfg).also { @@ -396,7 +397,7 @@ class AutotuneViewModel( // --- Internal --- private suspend fun resolveProfile() { - val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JSONObject()) + val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JsonObject(emptyMap())) profileFunction.getProfile()?.let { currentProfile -> profile = atProfileProvider.get().with( profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(value = it, activePlugin = null) } ?: currentProfile, @@ -426,7 +427,7 @@ class AutotuneViewModel( private suspend fun addWarnings(): String { val currentProfile = profileFunction.getProfile() ?: return rh.gs(app.aaps.core.ui.R.string.profileswitch_ismissing) - val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JSONObject()) + val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JsonObject(emptyMap())) val iCfg = currentProfile.iCfg val atProfile = atProfileProvider.get().with( profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(value = it, activePlugin = null) } ?: currentProfile, @@ -486,7 +487,7 @@ class AutotuneViewModel( } private suspend fun refreshState() { - val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JSONObject()) + val profileStore = profileRepository.profile.value ?: profileStoreProvider.get().with(JsonObject(emptyMap())) val profileList = profileStore.getProfileList().toMutableList() profileList.add(0, rh.gs(app.aaps.core.ui.R.string.active)) val profileNames = profileList.map { it.toString() } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt index 779ac1d2e7b4..2770211251d4 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt @@ -22,6 +22,8 @@ import app.aaps.core.objects.extensions.pureProfileFromJson import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.utils.MidnightUtils import app.aaps.plugins.aps.R +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -202,7 +204,7 @@ class ATProfile @Inject constructor( json.put("defaultProfile", profileName) json.put("store", store) json.put("startDate", dateUtil.toISOAsUTC(dateUtil.now())) - profileStore = profileStoreProvider.get().with(json) + profileStore = profileStoreProvider.get().with(Json.parseToJsonElement(json.toString()).jsonObject) } catch (e: JSONException) { aapsLogger.error(LTag.CORE, e.stackTraceToString()) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/DataSyncSelectorV3.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/DataSyncSelectorV3.kt index b6bdb83ac862..abeff4663fef 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/DataSyncSelectorV3.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/DataSyncSelectorV3.kt @@ -16,7 +16,6 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.LongNonKey import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.utils.JsonHelper import app.aaps.plugins.sync.nsclientV3.extensions.onlyNsIdAdded import app.aaps.plugins.sync.nsclientV3.keys.NsclientBooleanKey import app.aaps.plugins.sync.nsclientV3.keys.NsclientLongKey @@ -974,10 +973,10 @@ class DataSyncSelectorV3 @Inject constructor( // Snapshot once so the validity check and JSON read see the same store. val profileStore = profileRepository.profile.value ?: return if (!profileStore.allProfilesValid) return + // v3 needs `date`, and the store always carries one - it is written unconditionally by the + // single producer, ProfileRepositoryImpl.createAndStoreConvertedProfile, and pinned by a + // test there. This used to patch it in when missing, which could never fire. val profileJson = profileStore.getData() - // add for v3 - if (JsonHelper.safeGetLongAllowNull(profileJson, "date") == null) - profileJson.put("date", profileStore.getStartDate()) val now = dateUtil.now() if (nsClientV3Plugin.get().nsAdd("profile", DataSyncSelector.PairProfileStore(profileJson, now), "") == true) confirmLastProfileStore(now) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 4f51bb1eb1e9..d38c38c4bda4 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -760,7 +760,7 @@ class NSClientV3Plugin @Inject constructor( val data = (dataPair as DataSyncSelector.PairProfileStore).value try { nsClientRepository.addLog("► ADD $collection", "Sent ${dataPair.javaClass.simpleName} $progress", data) - nsAndroidClient?.createProfileStore(data.toKotlinxJson())?.let { result -> + nsAndroidClient?.createProfileStore(data)?.let { result -> when (result.response) { 200 -> nsClientRepository.addLog("◄ UPDATED", "OK ProfileStore") 201 -> nsClientRepository.addLog("◄ ADDED", "OK ProfileStore") diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt index a4a03b386a30..db13bd417175 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NsIncomingDataProcessor.kt @@ -55,6 +55,7 @@ import app.aaps.plugins.sync.nsclientV3.extensions.toRunningMode import app.aaps.plugins.sync.nsclientV3.extensions.toTemporaryBasal import app.aaps.plugins.sync.nsclientV3.extensions.toTemporaryTarget import app.aaps.plugins.sync.nsclientV3.extensions.toTherapyEvent +import app.aaps.plugins.sync.nsclientV3.json.JsonBridge.toKotlinxJson import org.json.JSONObject import javax.inject.Inject import javax.inject.Provider @@ -274,7 +275,7 @@ class NsIncomingDataProcessor @Inject constructor( if (config.AAPSCLIENT) !nsClient.masterOrPairedClientFlow.value else preferences.get(BooleanKey.NsClientAcceptProfileStore) || doFullSync if (accept) { - val store = profileStoreProvider.get().with(profileJson) + val store = profileStoreProvider.get().with(profileJson.toKotlinxJson()) val createdAt = store.getStartDate() val lastLocalChange = preferences.get(LongNonKey.LocalProfileLastChange) aapsLogger.debug(LTag.PROFILE, "Received profileStore: createdAt: $createdAt Local last modification: $lastLocalChange") diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/DataSyncSelectorXdripImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/DataSyncSelectorXdripImpl.kt index 26b18ef6436a..988aeae1c4f2 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/DataSyncSelectorXdripImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/DataSyncSelectorXdripImpl.kt @@ -13,7 +13,6 @@ import app.aaps.core.interfaces.sync.XDripBroadcast import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.LongNonKey import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.utils.JsonHelper import app.aaps.plugins.sync.xdrip.compose.XdripMvvmRepository import app.aaps.plugins.sync.xdrip.keys.XdripLongKey import dagger.Lazy @@ -652,10 +651,8 @@ class DataSyncSelectorXdripImpl @Inject constructor( // Snapshot once so the validity check and JSON read see the same store. val profileStore = profileRepository.profile.value ?: return if (!profileStore.allProfilesValid) return + // The store always carries `date` - see the same spot in DataSyncSelectorV3. val profileJson = profileStore.getData() - // add for v3 - if (JsonHelper.safeGetLongAllowNull(profileJson, "date") == null) - profileJson.put("date", profileStore.getStartDate()) val now = dateUtil.now() xdripPlugin.sendToXdrip("profile", DataSyncSelector.PairProfileStore(profileJson, now), "") confirmLastProfileStore(now) diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/DanaPump.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/DanaPump.kt index cf3ab3d4b5f2..20b1456790fb 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/DanaPump.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/DanaPump.kt @@ -19,6 +19,8 @@ import app.aaps.pump.dana.keys.DanaStringNonKey import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.json.JSONArray @@ -408,7 +410,7 @@ class DanaPump @Inject constructor( } catch (e: Exception) { return null } - return profileStoreProvider.get().with(json) + return profileStoreProvider.get().with(Json.parseToJsonElement(json.toString()).jsonObject) } return null } diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt index c0f01f70c873..a7bf011fed18 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt @@ -51,6 +51,8 @@ import dagger.android.AndroidInjector import dagger.android.DaggerApplication import dagger.android.HasAndroidInjector import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.json.JSONObject import org.junit.jupiter.api.BeforeEach import org.mockito.ArgumentMatchers.anyDouble @@ -331,7 +333,7 @@ open class TestBaseWithProfile : TestBase() { store.put(TESTPROFILENAME, JSONObject(validProfileJSON)) json.put("defaultProfile", TESTPROFILENAME) json.put("store", store) - return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(json) + return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(Json.parseToJsonElement(json.toString()).jsonObject) } fun getInvalidProfileStore1(): ProfileStore { @@ -340,7 +342,7 @@ open class TestBaseWithProfile : TestBase() { store.put(TESTPROFILENAME, JSONObject(invalidProfileJSON)) json.put("defaultProfile", TESTPROFILENAME) json.put("store", store) - return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(json) + return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(Json.parseToJsonElement(json.toString()).jsonObject) } fun getInvalidProfileStore2(): ProfileStore { @@ -350,6 +352,6 @@ open class TestBaseWithProfile : TestBase() { store.put("invalid", JSONObject(invalidProfileJSON)) json.put("defaultProfile", TESTPROFILENAME + "invalid") json.put("store", store) - return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(json) + return ProfileStoreObject(aapsLogger, activePlugin, config, rh, notificationManager, hardLimits, dateUtil).with(Json.parseToJsonElement(json.toString()).jsonObject) } } From 6918193b77576eb913cc3d90f69cc36f6a4e81b8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 13 Aug 2026 21:36:41 +0200 Subject: [PATCH 069/146] toPureNsJson on kotlinx JSON, with profile and Autotune coverage --- .../aaps/core/interfaces/profile/Profile.kt | 4 +- .../core/objects/profile/ProfileSealed.kt | 100 ++++++--------- .../core/objects/profile/PureNsJsonTest.kt | 115 ++++++++++++++++++ .../profile/ProfileRepositoryImplTest.kt | 80 ++++++++++++ .../plugins/aps/autotune/AutotunePlugin.kt | 6 +- .../plugins/aps/autotune/data/ATProfile.kt | 5 +- .../aps/autotune/AutotuneProfileJsonTest.kt | 78 ++++++++++++ 7 files changed, 320 insertions(+), 68 deletions(-) create mode 100644 core/objects/src/test/kotlin/app/aaps/core/objects/profile/PureNsJsonTest.kt create mode 100644 plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneProfileJsonTest.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt index b225366aa70b..c6ee981c0bc3 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt @@ -10,7 +10,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.interfaces.utils.Round -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface Profile { @@ -133,7 +133,7 @@ interface Profile { fun getTargetList(rh: ResourceHelper, dateUtil: DateUtil): String fun convertToNonCustomizedProfile(dateUtil: DateUtil): PureProfile - fun toPureNsJson(dateUtil: DateUtil): JSONObject + fun toPureNsJson(dateUtil: DateUtil): JsonObject fun getMaxDailyBasal(): Double fun baseBasalSum(): Double fun percentageBasalSum(): Double diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index a223eafc35a9..a56eb4d6055b 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -29,9 +29,15 @@ import app.aaps.core.objects.extensions.lowTargetBlockValueBySeconds import app.aaps.core.objects.extensions.shiftBlock import app.aaps.core.objects.extensions.shiftTargetBlock import app.aaps.core.objects.extensions.targetBlockValueBySeconds -import app.aaps.core.objects.extensions.toJson +import app.aaps.core.objects.extensions.toJsonObject import app.aaps.core.ui.R import app.aaps.core.utils.MidnightUtils +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.json.JSONArray import org.json.JSONObject import java.util.TimeZone @@ -381,69 +387,41 @@ sealed class ProfileSealed( timeZone = TimeZone.getDefault() ) - override fun toPureNsJson(dateUtil: DateUtil): JSONObject { - val o = JSONObject() - o.put("units", units.asText) - iCfg?.let { o.put("iCfg", it.toJson()) } - o.put("timezone", dateUtil.timeZoneByOffset(utcOffset)) - // SENS - val sens = JSONArray() + /** + * One `[{time, timeAsSeconds, value}, …]` schedule. + * + * The five schedules below used to be four near-identical loops with the entry shape written out + * each time. They differ only in which blocks set the boundaries and which accessor supplies the + * value, so that is all this takes: [durations] gives the hour boundaries, [valueAt] answers for + * each one. Note the value comes from the accessor, not from the block - the accessors apply + * percentage and time shift, which is why a raw block amount would be wrong here. + */ + private fun schedule(durations: List, valueAt: (Int) -> Double): JsonArray = buildJsonArray { var elapsedHours = 0L - isfBlocks.forEach { - sens.put( - JSONObject() - .put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") - .put("timeAsSeconds", T.hours(elapsedHours).secs()) - .put("value", getIsfTimeFromMidnight(T.hours(elapsedHours).secs().toInt())) - ) - elapsedHours += T.msecs(it.duration).hours() - } - o.put("sens", sens) - val carbratio = JSONArray() - elapsedHours = 0L - icBlocks.forEach { - carbratio.put( - JSONObject() - .put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") - .put("timeAsSeconds", T.hours(elapsedHours).secs()) - .put("value", getIcTimeFromMidnight(T.hours(elapsedHours).secs().toInt())) - ) - elapsedHours += T.msecs(it.duration).hours() - } - o.put("carbratio", carbratio) - val basal = JSONArray() - elapsedHours = 0L - basalBlocks.forEach { - basal.put( - JSONObject() - .put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") - .put("timeAsSeconds", T.hours(elapsedHours).secs()) - .put("value", getBasalTimeFromMidnight(T.hours(elapsedHours).secs().toInt())) - ) - elapsedHours += T.msecs(it.duration).hours() - } - o.put("basal", basal) - val targetLow = JSONArray() - val targetHigh = JSONArray() - elapsedHours = 0L - targetBlocks.forEach { - targetLow.put( - JSONObject() - .put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") - .put("timeAsSeconds", T.hours(elapsedHours).secs()) - .put("value", getTargetLowTimeFromMidnight(T.hours(elapsedHours).secs().toInt())) - ) - targetHigh.put( - JSONObject() - .put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") - .put("timeAsSeconds", T.hours(elapsedHours).secs()) - .put("value", getTargetHighTimeFromMidnight(T.hours(elapsedHours).secs().toInt())) + durations.forEach { duration -> + val seconds = T.hours(elapsedHours).secs() + add( + buildJsonObject { + put("time", NumberFormat.INTEGER_2_DIGITS.format(elapsedHours) + ":00") + put("timeAsSeconds", seconds) + put("value", valueAt(seconds.toInt())) + } ) - elapsedHours += T.msecs(it.duration).hours() + elapsedHours += T.msecs(duration).hours() } - o.put("target_low", targetLow) - o.put("target_high", targetHigh) - return o + } + + override fun toPureNsJson(dateUtil: DateUtil): JsonObject = buildJsonObject { + put("units", units.asText) + iCfg?.let { put("iCfg", it.toJsonObject()) } + put("timezone", dateUtil.timeZoneByOffset(utcOffset)) + put("sens", schedule(isfBlocks.map { it.duration }, ::getIsfTimeFromMidnight)) + put("carbratio", schedule(icBlocks.map { it.duration }, ::getIcTimeFromMidnight)) + put("basal", schedule(basalBlocks.map { it.duration }, ::getBasalTimeFromMidnight)) + // Low and high share the same boundaries, so they walk the same durations. + val targetDurations = targetBlocks.map { it.duration } + put("target_low", schedule(targetDurations, ::getTargetLowTimeFromMidnight)) + put("target_high", schedule(targetDurations, ::getTargetHighTimeFromMidnight)) } override fun getMaxDailyBasal(): Double = basalBlocks.maxByOrNull { it.amount }?.amount ?: 0.0 diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/PureNsJsonTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/PureNsJsonTest.kt new file mode 100644 index 000000000000..2b2fc4fdd49b --- /dev/null +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/PureNsJsonTest.kt @@ -0,0 +1,115 @@ +package app.aaps.core.objects.profile + +import android.content.Context +import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.objects.extensions.pureProfileFromJson +import app.aaps.shared.impl.utils.DateUtilImpl +import app.aaps.shared.tests.TestBase +import app.aaps.shared.tests.TestPumpPlugin +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.json.JSONObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.kotlin.whenever + +/** + * Pins the shape of the Nightscout profile document produced by `toPureNsJson`. + * + * Nothing covered this before: the sync tests call it only to build fixtures, and the one assertion + * in [ProfileSealedTest] is commented out. That is thin for a serializer whose output is written to + * the `profileJson` column, uploaded to Nightscout and read back by other AAPS builds - a change in + * the entry shape would surface as a wrong profile, not as a failing test. + * + * The five schedules are produced by one shared helper now, so the per-schedule assertions here are + * mostly about the boundaries each one walks. + */ +class PureNsJsonTest : TestBase() { + + @Mock lateinit var activePlugin: ActivePlugin + @Mock lateinit var rh: ResourceHelper + @Mock lateinit var context: Context + + /** Two blocks per schedule, switching at 02:00, so a boundary error cannot hide. */ + private val sourceProfile = + """{"dia":"5","carbratio":[{"time":"00:00","value":"30"},{"time":"02:00","value":"40"}], + "carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"100"},{"time":"02:00","value":"110"}], + "timezone":"UTC","basal":[{"time":"00:00","value":"0.1"},{"time":"02:00","value":"0.2"}], + "target_low":[{"time":"00:00","value":"4"},{"time":"02:00","value":"4.5"}], + "target_high":[{"time":"00:00","value":"5"},{"time":"02:00","value":"5.5"}], + "startDate":"1970-01-01T00:00:00.000Z","units":"mmol"}""" + + private lateinit var dateUtil: DateUtilImpl + + @BeforeEach fun prepare() { + dateUtil = DateUtilImpl(context) + whenever(activePlugin.activePump).thenReturn(TestPumpPlugin(rh)) + } + + private fun json() = + ProfileSealed.Pure(pureProfileFromJson(JSONObject(sourceProfile), dateUtil)!!, activePlugin).toPureNsJson(dateUtil) + + @Test + fun `carries the five schedules plus units and timezone`() { + assertThat(json().keys).containsExactly("units", "timezone", "sens", "carbratio", "basal", "target_low", "target_high") + } + + @Test + fun `each entry is time, timeAsSeconds and value`() { + val first = json()["sens"]!!.jsonArray[0].jsonObject + + assertThat(first.keys).containsExactly("time", "timeAsSeconds", "value") + assertThat(first["time"]!!.jsonPrimitive.content).isEqualTo("00:00") + assertThat(first["timeAsSeconds"]!!.jsonPrimitive.content).isEqualTo("0") + } + + @Test + fun `every schedule walks its own block boundaries`() { + val o = json() + for (key in listOf("sens", "carbratio", "basal", "target_low", "target_high")) { + val entries = o[key]!!.jsonArray + assertThat(entries).hasSize(2) + assertThat(entries[0].jsonObject["time"]!!.jsonPrimitive.content).isEqualTo("00:00") + assertThat(entries[1].jsonObject["time"]!!.jsonPrimitive.content).isEqualTo("02:00") + assertThat(entries[1].jsonObject["timeAsSeconds"]!!.jsonPrimitive.content).isEqualTo("7200") + } + } + + @Test + fun `values come through per schedule`() { + val o = json() + fun valueAt(key: String, index: Int) = o[key]!!.jsonArray[index].jsonObject["value"]!!.jsonPrimitive.content.toDouble() + + assertThat(valueAt("sens", 0)).isWithin(0.001).of(100.0) + assertThat(valueAt("sens", 1)).isWithin(0.001).of(110.0) + assertThat(valueAt("carbratio", 1)).isWithin(0.001).of(40.0) + assertThat(valueAt("basal", 1)).isWithin(0.001).of(0.2) + // Low and high share boundaries but not values. + assertThat(valueAt("target_low", 1)).isWithin(0.001).of(4.5) + assertThat(valueAt("target_high", 1)).isWithin(0.001).of(5.5) + } + + /** + * The document round-trips back into the profile it came from. + * + * This is the assertion that actually matters: whatever the text looks like, another AAPS build + * reading it must rebuild the same blocks. It is also what makes the one cosmetic difference from + * the previous `org.json` output harmless - integral values now render as `100.0` rather than + * `100`, and the reader does not care. + */ + @Test + fun `the document parses back into the same blocks`() { + val original = ProfileSealed.Pure(pureProfileFromJson(JSONObject(sourceProfile), dateUtil)!!, activePlugin) + + val reparsed = pureProfileFromJson(JSONObject(original.toPureNsJson(dateUtil).toString()), dateUtil)!! + + assertThat(reparsed.isfBlocks).isEqualTo(original.isfBlocks) + assertThat(reparsed.icBlocks).isEqualTo(original.icBlocks) + assertThat(reparsed.basalBlocks).isEqualTo(original.basalBlocks) + assertThat(reparsed.targetBlocks).isEqualTo(original.targetBlocks) + } +} diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt index 99072f29e5ce..272048342825 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileRepositoryImplTest.kt @@ -188,6 +188,86 @@ class ProfileRepositoryImplTest : TestBaseWithProfile() { // --------------------------------------------------------------------------------------------- /** Stub the pre-JSON per-profile keys so the legacy reader finds [names]. */ + /** A schedule the way a pre-JSON build wrote it. ASCII digits, whole hours, both time fields. */ + private fun legacySchedule(vararg hourToValue: Pair): String = + hourToValue.joinToString(",", "[", "]") { (hour, value) -> + val hh = if (hour < 10) "0$hour" else "$hour" + """{"time":"$hh:00","timeAsSeconds":${hour * 3600},"value":$value}""" + } + + /** + * One profile in the pre-JSON keys, shaped like real data rather than a placeholder. + * + * The values are taken from an actual 3.4.x install: mmol/L, several blocks per schedule, and the + * long decimals that come out of unit conversion. The existing [givenLegacyProfiles] fixture uses + * one round-numbered block per schedule, which cannot catch a boundary or precision mistake. + */ + private fun givenRealisticLegacyProfile(name: String) { + whenever(preferences.get(ProfileIntKey.AmountOfProfiles)).thenReturn(1) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedName, 0)).thenReturn(name) + whenever(preferences.get(ProfileComposedBooleanKey.LocalProfileNumberedMgdl, 0)).thenReturn(false) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIc, 0)) + .thenReturn(legacySchedule(0 to 8.1, 7 to 6.0, 10 to 8.0)) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedIsf, 0)) + .thenReturn(legacySchedule(0 to 9.523809523809524)) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedBasal, 0)) + .thenReturn(legacySchedule(0 to 1.0, 6 to 1.27, 11 to 1.6300000000000001)) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetLow, 0)) + .thenReturn(legacySchedule(0 to 5.5, 11 to 6.6000000000000005)) + whenever(preferences.get(ProfileComposedStringKey.LocalProfileNumberedTargetHigh, 0)) + .thenReturn(legacySchedule(0 to 5.5, 11 to 7.7)) + } + + /** + * The 3.4.x upgrade: legacy keys in, one JSON document out, nothing altered on the way. + * + * This is the highest-risk path in the profile rework - it runs once, silently, on every upgrading + * install, and a mistake in it corrupts profiles that were fine. It was verified against a real + * device carrying five such profiles; this pins the same guarantees so a future change cannot undo + * it. Times, values and the unit flag must survive exactly, including the long decimals, because + * blocks are stored as durations and rebuilt as start times. + */ + @Test + fun `legacy keys migrate into the document without altering any value`() = runTest { + givenRealisticLegacyProfile("Vsedni den") + whenever(config.APS).thenReturn(true) + + val sut = createSut() + + assertThat(sut.names()).containsExactly("Vsedni den") + val migrated = JSONObject(localWrites().last()).getJSONArray("profiles").getJSONObject(0) + assertThat(migrated.getString("name")).isEqualTo("Vsedni den") + assertThat(migrated.getBoolean("mgdl")).isFalse() + + fun schedule(key: String) = migrated.getJSONArray(key).let { array -> + (0 until array.length()).map { array.getJSONObject(it).getInt("timeAsSeconds") to array.getJSONObject(it).getDouble("value") } + } + + assertThat(schedule("ic")).containsExactly(0 to 8.1, 25200 to 6.0, 36000 to 8.0).inOrder() + assertThat(schedule("isf")).containsExactly(0 to 9.523809523809524).inOrder() + assertThat(schedule("basal")).containsExactly(0 to 1.0, 21600 to 1.27, 39600 to 1.6300000000000001).inOrder() + assertThat(schedule("targetLow")).containsExactly(0 to 5.5, 39600 to 6.6000000000000005).inOrder() + assertThat(schedule("targetHigh")).containsExactly(0 to 5.5, 39600 to 7.7).inOrder() + } + + /** The migrated profile must also be readable back, not merely written correctly. */ + @Test + fun `a migrated profile is usable as a profile`() = runTest { + givenRealisticLegacyProfile("Vsedni den") + whenever(config.APS).thenReturn(true) + + val sut = createSut() + val profile = sut.profiles.value.single() + + assertThat(profile.mgdl).isFalse() + assertThat(profile.ic).hasSize(3) + assertThat(profile.basal).hasSize(3) + assertThat(profile.target).hasSize(2) + // Durations, not start times: 00:00-07:00 is seven hours. + assertThat(profile.ic.first().duration).isEqualTo(7 * 3600 * 1000L) + assertThat(profile.basal.last().amount).isEqualTo(1.6300000000000001) + } + private fun givenLegacyProfiles(vararg names: String) { whenever(preferences.get(ProfileIntKey.AmountOfProfiles)).thenReturn(names.size) names.forEachIndexed { i, name -> diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index 7c94f77fb32b..94887961d363 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -425,13 +425,13 @@ class AutotunePlugin @Inject constructor( val json = JSONObject() json.put("lastNbDays", lastNbDays) json.put("lastRun", lastRun) - json.put("pumpProfile", pumpProfile.profile.toPureNsJson(dateUtil)) + json.put("pumpProfile", JSONObject(pumpProfile.profile.toPureNsJson(dateUtil).toString())) json.put("pumpProfileName", pumpProfile.profileName) json.put("pumpPeak", pumpProfile.peak) json.put("pumpDia", pumpProfile.dia) tunedProfile?.let { atProfile -> - json.put("tunedProfile", atProfile.profile.toPureNsJson(dateUtil)) - json.put("tunedCircadianProfile", atProfile.circadianProfile.toPureNsJson(dateUtil)) + json.put("tunedProfile", JSONObject(atProfile.profile.toPureNsJson(dateUtil).toString())) + json.put("tunedCircadianProfile", JSONObject(atProfile.circadianProfile.toPureNsJson(dateUtil).toString())) json.put("tunedProfileName", atProfile.profileName) json.put("tunedPeak", atProfile.peak) json.put("tunedDia", atProfile.dia) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt index 2770211251d4..d5cb439b0296 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/data/ATProfile.kt @@ -176,7 +176,8 @@ class ATProfile @Inject constructor( } fun data(circadian: Boolean = false): PureProfile? { - val json: JSONObject = profile.toPureNsJson(dateUtil) + // Still org.json below: the overrides use org.json arrays and pureProfileFromJson takes one. + val json = JSONObject(profile.toPureNsJson(dateUtil).toString()) try { if (circadian) { json.put("sens", jsonArray(pumpProfile.isfBlocks, avgISF / pumpProfileAvgISF)) @@ -200,7 +201,7 @@ class ATProfile @Inject constructor( if (profileName.isEmpty()) profileName = rh.gs(R.string.autotune_tunedprofile_name) try { - store.put(profileName, tunedProfile.toPureNsJson(dateUtil)) + store.put(profileName, JSONObject(tunedProfile.toPureNsJson(dateUtil).toString())) json.put("defaultProfile", profileName) json.put("store", store) json.put("startDate", dateUtil.toISOAsUTC(dateUtil.now())) diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneProfileJsonTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneProfileJsonTest.kt new file mode 100644 index 000000000000..e5246e126d09 --- /dev/null +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneProfileJsonTest.kt @@ -0,0 +1,78 @@ +package app.aaps.plugins.aps.autotune + +import app.aaps.core.objects.profile.ProfileSealed +import app.aaps.plugins.aps.autotune.data.ATProfile +import app.aaps.shared.tests.TestBaseWithProfile +import com.google.common.truth.Truth.assertThat +import org.json.JSONObject +import org.junit.jupiter.api.Test + +/** + * Autotune nests a profile document inside another JSON object in three places, and none of them had + * any coverage: `saveLastRun` was never called by a test, and `ATProfile.profileStore` was only ever + * mocked. + * + * That matters more than an untested getter, because the failure mode here is silent. + * `org.json.JSONObject.put(String, Any?)` accepts *anything*: handing it a value the library does not + * recognise stores the object and serialises it as a **quoted string** rather than a nested object. + * It compiles, it runs, and the damage only shows up in the written document - which is exactly what + * would have happened when `toPureNsJson` started returning a kotlinx tree. So these tests assert the + * nested value is a real object, not just that it is present. + */ +class AutotuneProfileJsonTest : TestBaseWithProfile() { + + private fun atProfile(): ATProfile = + ATProfile(preferences, profileUtil, dateUtil, rh, profileStoreProvider, aapsLogger) + .with(validProfile, someICfg) + + @Test + fun `the tuned profile store nests a real profile object`() { + val store = atProfile().also { it.profileName = "Tuned" }.profileStore() + + assertThat(store).isNotNull() + val nested = JSONObject(store!!.getData().toString()).getJSONObject("store") + // getJSONObject throws if the value were serialised as a string, which is the whole point. + val profile = nested.getJSONObject("Tuned") + assertThat(profile.has("basal")).isTrue() + assertThat(profile.getJSONArray("basal").length()).isAtLeast(1) + assertThat(profile.getJSONArray("basal").getJSONObject(0).has("timeAsSeconds")).isTrue() + } + + /** The store must be readable back as a profile, not merely well-formed. */ + @Test + fun `the tuned profile store parses back into a profile`() { + val store = atProfile().also { it.profileName = "Tuned" }.profileStore() + + val parsed = store!!.getSpecificProfile("Tuned") + + assertThat(parsed).isNotNull() + assertThat(parsed!!.basalBlocks).isNotEmpty() + assertThat(parsed.isfBlocks).isNotEmpty() + assertThat(parsed.icBlocks).isNotEmpty() + assertThat(parsed.targetBlocks).isNotEmpty() + } + + @Test + fun `data() rebuilds a profile from the tuned document`() { + val data = atProfile().data() + + assertThat(data).isNotNull() + assertThat(data!!.basalBlocks).isNotEmpty() + assertThat(data.targetBlocks).isNotEmpty() + } + + /** + * `ProfileSealed.Pure` and the tuned profile must serialise the same way - this is the path that + * feeds both the profile store above and `saveLastRun`. + */ + @Test + fun `a pure profile serialises as a nested object when embedded`() { + val embedded = JSONObject().put("pumpProfile", JSONObject(ProfileSealed.Pure(validProfile.value, activePlugin).toPureNsJson(dateUtil).toString())) + + val readBack = JSONObject(embedded.toString()).getJSONObject("pumpProfile") + + assertThat(readBack.has("sens")).isTrue() + assertThat(readBack.has("basal")).isTrue() + assertThat(readBack.getJSONArray("sens").getJSONObject(0).has("value")).isTrue() + } +} From fff05a5df1c1548dc6ab4999e67d8a10a30985e9 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 07:28:26 +0200 Subject: [PATCH 070/146] Command takes a result factory instead of a Dagger Provider --- .../app/aaps/core/interfaces/queue/Command.kt | 12 ++++-- .../queue/CommandQueueImplementation.kt | 40 +++++++++---------- .../queue/commands/CommandBolus.kt | 3 +- .../commands/CommandCancelExtendedBolus.kt | 3 +- .../queue/commands/CommandCancelTempBasal.kt | 3 +- .../queue/commands/CommandClearAlarms.kt | 5 +-- .../queue/commands/CommandCustomCommand.kt | 5 +-- .../queue/commands/CommandDeactivate.kt | 5 +-- .../queue/commands/CommandExtendedBolus.kt | 3 +- .../CommandInsightSetTBROverNotification.kt | 5 +-- .../queue/commands/CommandLoadEvents.kt | 5 +-- .../queue/commands/CommandLoadHistory.kt | 5 +-- .../queue/commands/CommandLoadTDDs.kt | 3 +- .../queue/commands/CommandReadStatus.kt | 5 +-- .../queue/commands/CommandSMBBolus.kt | 7 ++-- .../queue/commands/CommandSetProfile.kt | 5 +-- .../queue/commands/CommandSetUserSettings.kt | 5 +-- .../queue/commands/CommandStartPump.kt | 5 +-- .../queue/commands/CommandStopPump.kt | 5 +-- .../commands/CommandTempBasalAbsolute.kt | 3 +- .../queue/commands/CommandTempBasalPercent.kt | 3 +- .../queue/commands/CommandUpdateTime.kt | 5 +-- .../queue/commands/CommandBolusTest.kt | 2 +- .../CommandCancelExtendedBolusTest.kt | 2 +- .../commands/CommandCancelTempBasalTest.kt | 2 +- .../queue/commands/CommandClearAlarmsTest.kt | 2 +- .../commands/CommandCustomCommandTest.kt | 2 +- .../queue/commands/CommandDeactivateTest.kt | 2 +- .../commands/CommandExtendedBolusTest.kt | 2 +- ...ommandInsightSetTBROverNotificationTest.kt | 2 +- .../queue/commands/CommandLoadEventsTest.kt | 2 +- .../queue/commands/CommandLoadHistoryTest.kt | 2 +- .../queue/commands/CommandLoadTDDsTest.kt | 2 +- .../queue/commands/CommandReadStatusTest.kt | 2 +- .../queue/commands/CommandSMBBolusTest.kt | 2 +- .../queue/commands/CommandSetProfileTest.kt | 2 +- .../commands/CommandSetUserSettingsTest.kt | 2 +- .../queue/commands/CommandStartPumpTest.kt | 2 +- .../queue/commands/CommandStopPumpTest.kt | 2 +- .../commands/CommandTempBasalAbsoluteTest.kt | 2 +- .../commands/CommandTempBasalPercentTest.kt | 2 +- .../queue/commands/CommandUpdateTimeTest.kt | 2 +- 42 files changed, 83 insertions(+), 97 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt index a0216aa0c78b..6b239d0f8088 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt @@ -1,13 +1,19 @@ package app.aaps.core.interfaces.queue import app.aaps.core.interfaces.pump.PumpEnactResult -import javax.inject.Provider interface Command { val commandType: CommandType val callback: Callback? - val pumpEnactResultProvider: Provider + /** + * Makes a fresh [PumpEnactResult]. + * + * A plain factory function rather than a `javax.inject.Provider`: that type is Dagger vocabulary, + * and it appeared in the public API of this interface, which pinned every implementer and this + * whole file to a JVM-only DI framework. Android still injects a `Provider` and passes its `::get`. + */ + val pumpEnactResultProvider: () -> PumpEnactResult enum class CommandType { BOLUS, @@ -45,6 +51,6 @@ interface Command { * Return success = true to avoid command failed dialog */ fun cancel(commentResId: Int, success: Boolean = true) { - callback?.result(pumpEnactResultProvider.get().success(success).comment(commentResId))?.run() + callback?.result(pumpEnactResultProvider().success(success).comment(commentResId))?.run() } } \ No newline at end of file diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index fd31e45f4926..233864478aac 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -416,9 +416,9 @@ class CommandQueueImplementation @Inject constructor( detailedBolusInfo.insulin = constraintChecker.applyBolusConstraints(ConstraintObject(detailedBolusInfo.insulin, aapsLogger)).value() val bolusGeneration = bolusProgressData.start(detailedBolusInfo.insulin, isSMB = detailedBolusInfo.bolusType === BS.Type.SMB, isPriming = detailedBolusInfo.bolusType == BS.Type.PRIMING) if (detailedBolusInfo.bolusType == BS.Type.SMB) { - add(CommandSMBBolus(aapsLogger, rh, dateUtil, activePlugin, persistenceLayer, preferences, bolusProgressData, pumpEnactResultProvider, detailedBolusInfo, cb, bolusGeneration)) + add(CommandSMBBolus(aapsLogger, rh, dateUtil, activePlugin, persistenceLayer, preferences, bolusProgressData, pumpEnactResultProvider::get, detailedBolusInfo, cb, bolusGeneration)) } else { - add(CommandBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider, bolusProgressData, detailedBolusInfo, cb, type, bolusGeneration)) + add(CommandBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, bolusProgressData, detailedBolusInfo, cb, type, bolusGeneration)) if (type == CommandType.BOLUS) { // Notify Wear about upcoming bolus rxBus.send(EventMobileToWear(EventData.BolusProgress(percent = 0, status = rh.gs(app.aaps.core.ui.R.string.goingtodeliver, detailedBolusInfo.insulin)))) } @@ -448,7 +448,7 @@ class CommandQueueImplementation @Inject constructor( override suspend fun stopPump(): PumpEnactResult { val deferred = CompletableDeferred() - add(CommandStopPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandStopPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -459,7 +459,7 @@ class CommandQueueImplementation @Inject constructor( override suspend fun startPump(): PumpEnactResult { val deferred = CompletableDeferred() - add(CommandStartPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandStartPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -470,7 +470,7 @@ class CommandQueueImplementation @Inject constructor( override suspend fun setTBROverNotification(enable: Boolean): PumpEnactResult { val deferred = CompletableDeferred() - add(CommandInsightSetTBROverNotification(aapsLogger, rh, activePlugin, pumpEnactResultProvider, enable, object : Callback() { + add(CommandInsightSetTBROverNotification(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, enable, object : Callback() { override fun run() { deferred.complete(result) } @@ -500,7 +500,7 @@ class CommandQueueImplementation @Inject constructor( if (!enforceNew && isRunning(CommandType.TEMPBASAL)) return executingNowError() removeAll(CommandType.TEMPBASAL) val rateAfterConstraints = constraintChecker.applyBasalConstraints(ConstraintObject(absoluteRate, aapsLogger), profile).value() - add(CommandTempBasalAbsolute(aapsLogger, rh, activePlugin, pumpEnactResultProvider, rateAfterConstraints, durationInMinutes, enforceNew, tbrType, object : Callback() { + add(CommandTempBasalAbsolute(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, rateAfterConstraints, durationInMinutes, enforceNew, tbrType, object : Callback() { override fun run() { deferred.complete(result) } @@ -518,7 +518,7 @@ class CommandQueueImplementation @Inject constructor( if (!enforceNew && isRunning(CommandType.TEMPBASAL)) return executingNowError() removeAll(CommandType.TEMPBASAL) val percentAfterConstraints = constraintChecker.applyBasalPercentConstraints(ConstraintObject(percent, aapsLogger), profile).value() - add(CommandTempBasalPercent(aapsLogger, rh, activePlugin, pumpEnactResultProvider, percentAfterConstraints, durationInMinutes, enforceNew, tbrType, object : Callback() { + add(CommandTempBasalPercent(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, percentAfterConstraints, durationInMinutes, enforceNew, tbrType, object : Callback() { override fun run() { deferred.complete(result) } @@ -535,7 +535,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.EXTENDEDBOLUS)) return executingNowError() val rateAfterConstraints = constraintChecker.applyExtendedBolusConstraints(ConstraintObject(insulin, aapsLogger)).value() removeAll(CommandType.EXTENDEDBOLUS) - add(CommandExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider, rateAfterConstraints, durationInMinutes, object : Callback() { + add(CommandExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, rateAfterConstraints, durationInMinutes, object : Callback() { override fun run() { deferred.complete(result) } @@ -550,7 +550,7 @@ class CommandQueueImplementation @Inject constructor( synchronized(enqueueLock) { if (!enforceNew && isRunning(CommandType.TEMPBASAL)) return executingNowError() removeAll(CommandType.TEMPBASAL) - add(CommandCancelTempBasal(aapsLogger, rh, activePlugin, pumpSync, dateUtil, pumpEnactResultProvider, enforceNew, autoForced, object : Callback() { + add(CommandCancelTempBasal(aapsLogger, rh, activePlugin, pumpSync, dateUtil, pumpEnactResultProvider::get, enforceNew, autoForced, object : Callback() { override fun run() { deferred.complete(result) } @@ -565,7 +565,7 @@ class CommandQueueImplementation @Inject constructor( synchronized(enqueueLock) { if (isRunning(CommandType.EXTENDEDBOLUS)) return executingNowError() removeAll(CommandType.EXTENDEDBOLUS) - add(CommandCancelExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandCancelExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -597,7 +597,7 @@ class CommandQueueImplementation @Inject constructor( val deferred = CompletableDeferred() add( CommandSetProfile( - aapsLogger, rh, smsCommunicator.get(), activePlugin, dateUtil, this, config, persistenceLayer, pumpEnactResultProvider, + aapsLogger, rh, smsCommunicator.get(), activePlugin, dateUtil, this, config, persistenceLayer, pumpEnactResultProvider::get, profile, hasNsId, object : Callback() { override fun run() { deferred.complete(result) @@ -614,7 +614,7 @@ class CommandQueueImplementation @Inject constructor( return executingNowError() } val deferred = CompletableDeferred() - add(CommandReadStatus(aapsLogger, rh, activePlugin, localAlertUtils.get(), pumpEnactResultProvider, reason, object : Callback() { + add(CommandReadStatus(aapsLogger, rh, activePlugin, localAlertUtils.get(), pumpEnactResultProvider::get, reason, object : Callback() { override fun run() { deferred.complete(result) } @@ -640,7 +640,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.LOAD_HISTORY)) return executingNowError() removeAll(CommandType.LOAD_HISTORY) val deferred = CompletableDeferred() - add(CommandLoadHistory(aapsLogger, rh, activePlugin, pumpEnactResultProvider, type, object : Callback() { + add(CommandLoadHistory(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, type, object : Callback() { override fun run() { deferred.complete(result) } @@ -653,7 +653,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.SET_USER_SETTINGS)) return executingNowError() removeAll(CommandType.SET_USER_SETTINGS) val deferred = CompletableDeferred() - add(CommandSetUserSettings(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandSetUserSettings(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -666,7 +666,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.LOAD_TDD)) return executingNowError() removeAll(CommandType.LOAD_TDD) val deferred = CompletableDeferred() - add(CommandLoadTDDs(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandLoadTDDs(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -679,7 +679,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.LOAD_EVENTS)) return executingNowError() removeAll(CommandType.LOAD_EVENTS) val deferred = CompletableDeferred() - add(CommandLoadEvents(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandLoadEvents(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -692,7 +692,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.CLEAR_ALARMS)) return executingNowError() removeAll(CommandType.CLEAR_ALARMS) val deferred = CompletableDeferred() - add(CommandClearAlarms(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandClearAlarms(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -705,7 +705,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.DEACTIVATE)) return executingNowError() removeAll(CommandType.DEACTIVATE) val deferred = CompletableDeferred() - add(CommandDeactivate(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandDeactivate(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -718,7 +718,7 @@ class CommandQueueImplementation @Inject constructor( if (isRunning(CommandType.UPDATE_TIME)) return executingNowError() removeAll(CommandType.UPDATE_TIME) val deferred = CompletableDeferred() - add(CommandUpdateTime(aapsLogger, rh, activePlugin, pumpEnactResultProvider, object : Callback() { + add(CommandUpdateTime(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, object : Callback() { override fun run() { deferred.complete(result) } @@ -731,7 +731,7 @@ class CommandQueueImplementation @Inject constructor( if (isCustomCommandInQueue(customCommand.javaClass)) return executingNowError() removeAllCustomCommands(customCommand.javaClass) val deferred = CompletableDeferred() - add(CommandCustomCommand(aapsLogger, activePlugin, pumpEnactResultProvider, customCommand, object : Callback() { + add(CommandCustomCommand(aapsLogger, activePlugin, pumpEnactResultProvider::get, customCommand, object : Callback() { override fun run() { deferred.complete(result) } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandBolus.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandBolus.kt index 7f5acfe7830a..0028fba1c72e 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandBolus.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandBolus.kt @@ -9,13 +9,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandBolus( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val bolusProgressData: BolusProgressData, private val detailedBolusInfo: DetailedBolusInfo, override val callback: Callback?, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolus.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolus.kt index 493a23408af2..027d71836a4f 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolus.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolus.kt @@ -7,13 +7,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandCancelExtendedBolus( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasal.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasal.kt index e5eb93804578..68cd772cb1da 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasal.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasal.kt @@ -9,7 +9,6 @@ import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil -import javax.inject.Provider class CommandCancelTempBasal( private val aapsLogger: AAPSLogger, @@ -17,7 +16,7 @@ class CommandCancelTempBasal( private val activePlugin: ActivePlugin, private val pumpSync: PumpSync, private val dateUtil: DateUtil, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val enforceNew: Boolean, /** true if called by detection of pump in suspend mode */ private val autoForced: Boolean, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarms.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarms.kt index 9b2093472f7c..5c73d31feefa 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarms.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarms.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandClearAlarms( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandClearAlarms( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommand.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommand.kt index 053366c340ff..6f32c0d9121e 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommand.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommand.kt @@ -7,12 +7,11 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.queue.CustomCommand -import javax.inject.Provider class CommandCustomCommand( private val aapsLogger: AAPSLogger, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, val customCommand: CustomCommand, override val callback: Callback?, ) : Command { @@ -21,7 +20,7 @@ class CommandCustomCommand( override suspend fun execute(): PumpEnactResult { val r = activePlugin.activePump.executeCustomCommand(customCommand) - ?: pumpEnactResultProvider.get().success(true).enacted(false) + ?: pumpEnactResultProvider().success(true).enacted(false) aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") return r } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandDeactivate.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandDeactivate.kt index 12c5e7afc868..cb504412a964 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandDeactivate.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandDeactivate.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandDeactivate( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandDeactivate( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolus.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolus.kt index 26a98892578f..c05dd87283ca 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolus.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolus.kt @@ -7,13 +7,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandExtendedBolus( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val insulin: Double, private val durationInMinutes: Int, override val callback: Callback?, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotification.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotification.kt index 2f7619a506c1..aec7b3d78015 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotification.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotification.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandInsightSetTBROverNotification( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val enabled: Boolean, override val callback: Callback?, ) : Command { @@ -28,7 +27,7 @@ class CommandInsightSetTBROverNotification( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadEvents.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadEvents.kt index 06ce70e33b39..e476d6052020 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadEvents.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadEvents.kt @@ -10,13 +10,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandLoadEvents( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -28,7 +27,7 @@ class CommandLoadEvents( is Dana -> pump.loadEvents() is Diaconn -> pump.loadHistory() is Medtrum -> pump.loadEvents() - else -> pumpEnactResultProvider.get().success(true).enacted(false) + else -> pumpEnactResultProvider().success(true).enacted(false) } aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${result.success} enacted: ${result.enacted}") return result diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistory.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistory.kt index 52ab1a45d012..d3af228c7e11 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistory.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistory.kt @@ -9,13 +9,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandLoadHistory( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val type: Byte, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandLoadHistory( val result = when (pump) { is Dana -> pump.loadHistory(type) is Diaconn -> pump.loadHistory() - else -> pumpEnactResultProvider.get().success(true).enacted(false) + else -> pumpEnactResultProvider().success(true).enacted(false) } aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${result.success} enacted: ${result.enacted}") return result diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDs.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDs.kt index ecad3b228c57..808f4b273b7c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDs.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDs.kt @@ -7,13 +7,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandLoadTDDs( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandReadStatus.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandReadStatus.kt index 5cbd25b13d25..04eab204b8e8 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandReadStatus.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandReadStatus.kt @@ -9,14 +9,13 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandReadStatus( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, private val localAlertUtils: LocalAlertUtils, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, val reason: String, override val callback: Callback?, ) : Command { @@ -28,7 +27,7 @@ class CommandReadStatus( localAlertUtils.reportPumpStatusRead() aapsLogger.debug(LTag.PUMPQUEUE, "CommandReadStatus executed. Reason: $reason") val pump = activePlugin.activePump - val result = pumpEnactResultProvider.get().success(false) + val result = pumpEnactResultProvider().success(false) val lastConnection = pump.lastDataTime.value if (lastConnection > System.currentTimeMillis() - T.mins(1).msecs()) result.success(true) return result diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolus.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolus.kt index 803871696f5b..99bf6cd11282 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolus.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolus.kt @@ -14,7 +14,6 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.IntKey import app.aaps.core.keys.interfaces.Preferences -import javax.inject.Provider class CommandSMBBolus( private val aapsLogger: AAPSLogger, @@ -24,7 +23,7 @@ class CommandSMBBolus( private val persistenceLayer: PersistenceLayer, private val preferences: Preferences, private val bolusProgressData: BolusProgressData, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val detailedBolusInfo: DetailedBolusInfo, override val callback: Callback?, private val bolusGeneration: Long, @@ -38,11 +37,11 @@ class CommandSMBBolus( aapsLogger.debug(LTag.PUMPQUEUE, "Last bolus: $lastBolusTime ${dateUtil.dateAndTimeAndSecondsString(lastBolusTime)}") if (lastBolusTime != 0L && lastBolusTime + T.mins(preferences.get(IntKey.ApsMaxSmbFrequency).toLong()).msecs() > dateUtil.now()) { aapsLogger.debug(LTag.APS, "SMB requested but still in ${preferences.get(IntKey.ApsMaxSmbFrequency)} min interval") - r = pumpEnactResultProvider.get().enacted(false).success(false).comment("SMB requested but still in ${preferences.get(IntKey.ApsMaxSmbFrequency)} min interval") + r = pumpEnactResultProvider().enacted(false).success(false).comment("SMB requested but still in ${preferences.get(IntKey.ApsMaxSmbFrequency)} min interval") } else if (detailedBolusInfo.deliverAtTheLatest != 0L && detailedBolusInfo.deliverAtTheLatest + T.mins(1).msecs() > System.currentTimeMillis()) { r = activePlugin.activePump.deliverTreatment(detailedBolusInfo) } else { - r = pumpEnactResultProvider.get().enacted(false).success(false).comment("SMB request too old") + r = pumpEnactResultProvider().enacted(false).success(false).comment("SMB request too old") aapsLogger.debug(LTag.PUMPQUEUE, "SMB bolus canceled. deliverAt: " + dateUtil.dateAndTimeString(detailedBolusInfo.deliverAtTheLatest)) } aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted}") diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetProfile.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetProfile.kt index 651cebeb48c6..7d764eaad177 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetProfile.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetProfile.kt @@ -14,7 +14,6 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator import app.aaps.core.interfaces.utils.DateUtil -import javax.inject.Provider class CommandSetProfile( private val aapsLogger: AAPSLogger, @@ -25,7 +24,7 @@ class CommandSetProfile( private val commandQueue: CommandQueue, private val config: Config, private val persistenceLayer: PersistenceLayer, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val profile: EffectiveProfile, private val hasNsId: Boolean, override val callback: Callback?, @@ -36,7 +35,7 @@ class CommandSetProfile( override suspend fun execute(): PumpEnactResult { if (commandQueue.isThisProfileSet(profile) && persistenceLayer.getEffectiveProfileSwitchActiveAt(dateUtil.now()) != null) { aapsLogger.debug(LTag.PUMPQUEUE, "Correct profile already set. profile: $profile") - return pumpEnactResultProvider.get().success(true).enacted(false) + return pumpEnactResultProvider().success(true).enacted(false) } val r = activePlugin.activePump.setNewBasalProfile(profile) aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${r.success} enacted: ${r.enacted} profile: $profile") diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettings.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettings.kt index ff781da1004d..2d5cf768ee30 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettings.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettings.kt @@ -10,13 +10,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandSetUserSettings( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -28,7 +27,7 @@ class CommandSetUserSettings( is Dana -> pump.setUserOptions() is Diaconn -> pump.setUserOptions() is Medtrum -> pump.setUserOptions() - else -> pumpEnactResultProvider.get().success(true).enacted(false) + else -> pumpEnactResultProvider().success(true).enacted(false) } aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${result.success} enacted: ${result.enacted}") return result diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStartPump.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStartPump.kt index 066448479403..5f435ef50490 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStartPump.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStartPump.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandStartPump( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandStartPump( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStopPump.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStopPump.kt index d5945dd13d94..a015217cc720 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStopPump.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandStopPump.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandStopPump( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandStopPump( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsolute.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsolute.kt index ea14eb77f748..490d6a3faac1 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsolute.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsolute.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandTempBasalAbsolute( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val absoluteRate: Double, private val durationInMinutes: Int, private val enforceNew: Boolean, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercent.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercent.kt index 84740dc57eff..81750fdae6c5 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercent.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercent.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandTempBasalPercent( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, private val percent: Int, private val durationInMinutes: Int, private val enforceNew: Boolean, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTime.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTime.kt index b24d86ca16f2..0b43bcb1a137 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTime.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTime.kt @@ -8,13 +8,12 @@ import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.resources.ResourceHelper -import javax.inject.Provider class CommandUpdateTime( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val activePlugin: ActivePlugin, - override val pumpEnactResultProvider: Provider, + override val pumpEnactResultProvider: () -> PumpEnactResult, override val callback: Callback?, ) : Command { @@ -27,7 +26,7 @@ class CommandUpdateTime( aapsLogger.debug(LTag.PUMPQUEUE, "Result success: ${it.success} enacted: ${it.enacted}") } } else { - pumpEnactResultProvider.get().success(true).enacted(false) + pumpEnactResultProvider().success(true).enacted(false) } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandBolusTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandBolusTest.kt index fe86f103b409..12961e73c690 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandBolusTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandBolusTest.kt @@ -25,7 +25,7 @@ class CommandBolusTest : TestBaseWithProfile() { private fun newCommand(type: Command.CommandType = Command.CommandType.BOLUS, callback: Callback? = null) = CommandBolus( - aapsLogger, rh, activePlugin, pumpEnactResultProvider, bolusProgressData, + aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, bolusProgressData, info, callback, type, BOLUS_GENERATION ) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolusTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolusTest.kt index 1081aae8ef54..4b546cccd0e3 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolusTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelExtendedBolusTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandCancelExtendedBolusTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandCancelExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandCancelExtendedBolus(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute returns pump's cancelExtendedBolus result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasalTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasalTest.kt index 74ba754ba56f..c3f14ab250ed 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasalTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCancelTempBasalTest.kt @@ -31,7 +31,7 @@ class CommandCancelTempBasalTest : TestBaseWithProfile() { autoForced: Boolean = false, callback: Callback? = null ) = CommandCancelTempBasal( - aapsLogger, rh, activePlugin, pumpSync, dateUtil, pumpEnactResultProvider, + aapsLogger, rh, activePlugin, pumpSync, dateUtil, pumpEnactResultProvider::get, enforceNew, autoForced, callback ) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarmsTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarmsTest.kt index 76f47c2471fe..deb8f01c3d5c 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarmsTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandClearAlarmsTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandClearAlarmsTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandClearAlarms(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandClearAlarms(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Medtrum pump returns pump's clearAlarms result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommandTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommandTest.kt index 5bb354d6d942..ec84b5bed39e 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommandTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandCustomCommandTest.kt @@ -20,7 +20,7 @@ class CommandCustomCommandTest : TestBaseWithProfile() { } private fun newCommand(callback: Callback? = null) = - CommandCustomCommand(aapsLogger, activePlugin, pumpEnactResultProvider, customCommand, callback) + CommandCustomCommand(aapsLogger, activePlugin, pumpEnactResultProvider::get, customCommand, callback) @Test fun `execute returns pump's executeCustomCommand result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandDeactivateTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandDeactivateTest.kt index 2009d8a8ae99..2962d61f3a36 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandDeactivateTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandDeactivateTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandDeactivateTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandDeactivate(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandDeactivate(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Medtrum pump returns pump's deactivate result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolusTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolusTest.kt index 03c845145ec5..1bc88f0a587a 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolusTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandExtendedBolusTest.kt @@ -20,7 +20,7 @@ class CommandExtendedBolusTest : TestBaseWithProfile() { durationInMinutes: Int = 30, callback: Callback? = null ) = CommandExtendedBolus( - aapsLogger, rh, activePlugin, pumpEnactResultProvider, + aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, insulin, durationInMinutes, callback ) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotificationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotificationTest.kt index e9b6d75b059d..b3fdde074ef9 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotificationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandInsightSetTBROverNotificationTest.kt @@ -17,7 +17,7 @@ import org.mockito.kotlin.whenever class CommandInsightSetTBROverNotificationTest : TestBaseWithProfile() { private fun newCommand(enabled: Boolean = true, callback: Callback? = null) = - CommandInsightSetTBROverNotification(aapsLogger, rh, activePlugin, pumpEnactResultProvider, enabled, callback) + CommandInsightSetTBROverNotification(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, enabled, callback) @Test fun `execute on Insight pump returns pump's setTBROverNotification result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadEventsTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadEventsTest.kt index a6338ceda15a..db427e48803b 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadEventsTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadEventsTest.kt @@ -18,7 +18,7 @@ import org.mockito.kotlin.whenever class CommandLoadEventsTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandLoadEvents(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandLoadEvents(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Dana pump returns pump's loadEvents result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistoryTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistoryTest.kt index 9f03aaab4741..17db11e5c395 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistoryTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadHistoryTest.kt @@ -18,7 +18,7 @@ import org.mockito.kotlin.whenever class CommandLoadHistoryTest : TestBaseWithProfile() { private fun newCommand(type: Byte = 0, callback: Callback? = null) = - CommandLoadHistory(aapsLogger, rh, activePlugin, pumpEnactResultProvider, type, callback) + CommandLoadHistory(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, type, callback) @Test fun `execute on Dana pump returns pump's loadHistory result and passes type`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDsTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDsTest.kt index d4d7e51c6158..e0f3ad3eb18a 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDsTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandLoadTDDsTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandLoadTDDsTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandLoadTDDs(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandLoadTDDs(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute returns pump's loadTDDs result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandReadStatusTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandReadStatusTest.kt index ccfba62f3fe6..778804ece1ea 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandReadStatusTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandReadStatusTest.kt @@ -21,7 +21,7 @@ class CommandReadStatusTest : TestBaseWithProfile() { @Mock lateinit var localAlertUtils: LocalAlertUtils private fun newCommand(reason: String = "test reason", callback: Callback? = null) = - CommandReadStatus(aapsLogger, rh, activePlugin, localAlertUtils, pumpEnactResultProvider, reason, callback) + CommandReadStatus(aapsLogger, rh, activePlugin, localAlertUtils, pumpEnactResultProvider::get, reason, callback) private fun pumpWithLastData(lastDataTime: Long): PumpWithConcentration { val pump = mock() diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolusTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolusTest.kt index 380c028f6efc..60e705c68aef 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolusTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSMBBolusTest.kt @@ -29,7 +29,7 @@ class CommandSMBBolusTest : TestBaseWithProfile() { private fun newCommand(info: DetailedBolusInfo, callback: Callback? = null) = CommandSMBBolus( aapsLogger, rh, dateUtil, activePlugin, persistenceLayer, preferences, bolusProgressData, - pumpEnactResultProvider, info, callback, BOLUS_GENERATION + pumpEnactResultProvider::get, info, callback, BOLUS_GENERATION ) private fun smbInfo(deliverAtTheLatest: Long = System.currentTimeMillis()) = diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetProfileTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetProfileTest.kt index 89bf37e78a92..eb265c22dfc8 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetProfileTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetProfileTest.kt @@ -32,7 +32,7 @@ class CommandSetProfileTest : TestBaseWithProfile() { private fun newCommand(hasNsId: Boolean = false, callback: Callback? = null) = CommandSetProfile( aapsLogger, rh, smsCommunicator, activePlugin, dateUtil, commandQueue, config, persistenceLayer, - pumpEnactResultProvider, effectiveProfile, hasNsId, callback + pumpEnactResultProvider::get, effectiveProfile, hasNsId, callback ) @Test diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettingsTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettingsTest.kt index 95f57f8d4aed..0e8e747c2939 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettingsTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandSetUserSettingsTest.kt @@ -18,7 +18,7 @@ import org.mockito.kotlin.whenever class CommandSetUserSettingsTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandSetUserSettings(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandSetUserSettings(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Dana pump returns pump's setUserOptions result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStartPumpTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStartPumpTest.kt index ea08da51c93f..469fe845b32e 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStartPumpTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStartPumpTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandStartPumpTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandStartPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandStartPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Insight pump returns pump's startPump result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStopPumpTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStopPumpTest.kt index ab556bff5c8d..3a1a1f429a77 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStopPumpTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandStopPumpTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandStopPumpTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandStopPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandStopPump(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Insight pump returns pump's stopPump result`() = runTest { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsoluteTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsoluteTest.kt index e78240490005..c9961d1d6831 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsoluteTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalAbsoluteTest.kt @@ -23,7 +23,7 @@ class CommandTempBasalAbsoluteTest : TestBaseWithProfile() { tbrType: PumpSync.TemporaryBasalType = PumpSync.TemporaryBasalType.NORMAL, callback: Callback? = null ) = CommandTempBasalAbsolute( - aapsLogger, rh, activePlugin, pumpEnactResultProvider, + aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, absoluteRate, durationInMinutes, enforceNew, tbrType, callback ) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercentTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercentTest.kt index 20d6cf7ad24e..cb9e2171c88b 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercentTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandTempBasalPercentTest.kt @@ -26,7 +26,7 @@ class CommandTempBasalPercentTest : TestBaseWithProfile() { tbrType: PumpSync.TemporaryBasalType = PumpSync.TemporaryBasalType.NORMAL, callback: Callback? = null ) = CommandTempBasalPercent( - aapsLogger, rh, activePlugin, pumpEnactResultProvider, + aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, percent, durationInMinutes, enforceNew, tbrType, callback ) diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTimeTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTimeTest.kt index 1b5984d1c9cc..c1d81da0f59b 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTimeTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/commands/CommandUpdateTimeTest.kt @@ -16,7 +16,7 @@ import org.mockito.kotlin.whenever class CommandUpdateTimeTest : TestBaseWithProfile() { private fun newCommand(callback: Callback? = null) = - CommandUpdateTime(aapsLogger, rh, activePlugin, pumpEnactResultProvider, callback) + CommandUpdateTime(aapsLogger, rh, activePlugin, pumpEnactResultProvider::get, callback) @Test fun `execute on Medtrum pump returns pump's updateTime result`() = runTest { From 3128760fdd7f36735c1bf74ccbe17c1fae57832e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 12:26:52 +0200 Subject: [PATCH 071/146] Temp target presets, version definition and NS log on kotlinx JSON --- .../interfaces/nsclient/NSClientRepository.kt | 8 -- .../tempTargets/TempTargetPresetExtensions.kt | 58 +++++++----- .../versionChecker/VersionDefinition.kt | 4 +- .../TempTargetPresetExtensionsTest.kt | 94 +++++++++++++++++++ .../di/PluginsConstraintsModule.kt | 5 +- .../versionChecker/AllowedVersions.kt | 17 +++- .../versionChecker/VersionCheckerUtilsImpl.kt | 4 +- .../VersionCheckerUtilsKtTest.kt | 5 +- .../versionChecker/AllowedVersionsTest.kt | 9 +- .../nsclientV3/services/NSClientV3Service.kt | 2 +- 10 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt index 7911d25d1cc7..9babf56d4be0 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt @@ -1,10 +1,7 @@ package app.aaps.core.interfaces.nsclient import kotlinx.coroutines.flow.StateFlow -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import org.json.JSONObject /** * Repository interface for NSClient UI state management. @@ -46,11 +43,6 @@ interface NSClientRepository { addLog(action, logText, null as JsonElement?) } - /** Add a new log entry with JSONObject payload */ - fun addLog(action: String, logText: String?, json: JSONObject) { - val jsonObject = json.let { Json.parseToJsonElement(it.toString()) as JsonObject } - addLog(action, logText, jsonObject) - } /** Clear all log entries */ fun clearLog() diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt index 2b2782ced130..8db6dd490872 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt @@ -5,8 +5,15 @@ import app.aaps.core.data.model.TT import app.aaps.core.data.model.TTPreset import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences -import org.json.JSONArray -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put /** * Parse JSON string into a list of TTPreset. @@ -19,17 +26,17 @@ fun String.toTTPresets(): List { if (isEmpty() || this == "[]") { emptyList() } else { - val jsonArray = JSONArray(this) - (0 until jsonArray.length()).map { i -> - val obj = jsonArray.getJSONObject(i) - val reason = TT.Reason.fromString(obj.getString("reason")) + (Json.parseToJsonElement(this) as JsonArray).map { element -> + val obj = element.jsonObject + // `content` rather than the typed accessors so a quoted number still reads, which is + // what org.json's getDouble/getLong did and what documents in the wild contain. TTPreset( - id = obj.getString("id"), - name = if (obj.has("name") && !obj.isNull("name")) obj.getString("name") else null, - reason = reason, - targetValue = obj.getDouble("targetValue"), - duration = obj.getLong("duration"), - isDeletable = obj.getBoolean("isDeletable") + id = obj.getValue("id").jsonPrimitive.content, + name = (obj["name"] as? JsonPrimitive)?.takeUnless { it is JsonNull }?.content, + reason = TT.Reason.fromString(obj.getValue("reason").jsonPrimitive.content), + targetValue = obj.getValue("targetValue").jsonPrimitive.content.toDouble(), + duration = obj.getValue("duration").jsonPrimitive.content.toDouble().toLong(), + isDeletable = obj.getValue("isDeletable").jsonPrimitive.content.toBooleanStrict() ) } } @@ -42,21 +49,20 @@ fun String.toTTPresets(): List { * Convert a list of TTPreset to JSON string. * nameRes is NOT persisted — Android resource IDs change between builds. */ -fun List.toJson(): String { - val jsonArray = JSONArray() - forEach { preset -> - val obj = JSONObject().apply { - put("id", preset.id) - preset.name?.let { put("name", it) } - put("reason", preset.reason.text) - put("targetValue", preset.targetValue) - put("duration", preset.duration) - put("isDeletable", preset.isDeletable) +fun List.toJson(): String = + buildJsonArray { + this@toJson.forEach { preset -> + addJsonObject { + put("id", preset.id) + // Absent rather than null, as before - the reader treats the two the same either way. + preset.name?.let { put("name", it) } + put("reason", preset.reason.text) + put("targetValue", preset.targetValue) + put("duration", preset.duration) + put("isDeletable", preset.isDeletable) + } } - jsonArray.put(obj) - } - return jsonArray.toString() -} + }.toString() /** * Get all TT presets from preferences. diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt index b13a2c84220d..822ff16a6301 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt @@ -1,8 +1,8 @@ package app.aaps.core.interfaces.versionChecker -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject fun interface VersionDefinition { - fun invoke(): JSONObject + fun invoke(): JsonObject } \ No newline at end of file diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt new file mode 100644 index 000000000000..e2c03f2232a5 --- /dev/null +++ b/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt @@ -0,0 +1,94 @@ +package app.aaps.core.interfaces.tempTargets + +import app.aaps.core.data.model.TT +import app.aaps.core.data.model.TTPreset +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +/** + * Pins the temp-target preset list, which is a **stored preference** on every existing install. + * + * There was no test on the parser at all. That is thin for a reader whose failure mode is silent: it + * catches everything and answers with an empty list, so a regression does not crash - the user simply + * finds their presets gone. These tests were written against the `org.json` implementation and kept + * unchanged across the move to kotlinx, so they say the new reader accepts exactly what the old one + * wrote. + */ +class TempTargetPresetExtensionsTest { + + private val presets = listOf( + TTPreset(id = "eating", name = "Eating soon", reason = TT.Reason.EATING_SOON, targetValue = 90.0, duration = 2_700_000L, isDeletable = false), + TTPreset(id = "activity", name = null, reason = TT.Reason.ACTIVITY, targetValue = 140.0, duration = 5_400_000L, isDeletable = true) + ) + + @Test + fun `a written list reads back unchanged`() { + assertThat(presets.toJson().toTTPresets()).isEqualTo(presets) + } + + /** Exactly what an existing install has stored, written by the previous org.json code. */ + @Test + fun `an already stored document still parses`() { + val stored = """[{"id":"eating","name":"Eating soon","reason":"Eating Soon","targetValue":90,""" + + """"duration":2700000,"isDeletable":false},{"id":"activity","reason":"Activity",""" + + """"targetValue":140,"duration":5400000,"isDeletable":true}]""" + + val parsed = stored.toTTPresets() + + assertThat(parsed).hasSize(2) + assertThat(parsed[0].id).isEqualTo("eating") + assertThat(parsed[0].name).isEqualTo("Eating soon") + assertThat(parsed[0].reason).isEqualTo(TT.Reason.EATING_SOON) + assertThat(parsed[0].targetValue).isEqualTo(90.0) + assertThat(parsed[0].duration).isEqualTo(2_700_000L) + assertThat(parsed[0].isDeletable).isFalse() + // A missing "name" is null, not an empty string. + assertThat(parsed[1].name).isNull() + assertThat(parsed[1].isDeletable).isTrue() + } + + /** Numbers written as quoted strings occur in the wild and were accepted before. */ + @Test + fun `quoted numbers are accepted`() { + val quoted = """[{"id":"a","reason":"Activity","targetValue":"140.5","duration":"5400000","isDeletable":"true"}]""" + + val parsed = quoted.toTTPresets() + + assertThat(parsed).hasSize(1) + assertThat(parsed[0].targetValue).isEqualTo(140.5) + assertThat(parsed[0].duration).isEqualTo(5_400_000L) + assertThat(parsed[0].isDeletable).isTrue() + } + + /** An explicit JSON null for name behaves like an absent one. */ + @Test + fun `an explicit null name is treated as absent`() { + val withNull = """[{"id":"a","name":null,"reason":"Activity","targetValue":140,"duration":1,"isDeletable":true}]""" + + assertThat(withNull.toTTPresets().single().name).isNull() + } + + @Test + fun `empty and blank documents give an empty list`() { + assertThat("".toTTPresets()).isEmpty() + assertThat("[]".toTTPresets()).isEmpty() + } + + /** + * Unreadable input must never propagate. The list is read on a settings screen and in the wizard; + * throwing there would take the screen down over a corrupt preference. + */ + @Test + fun `malformed input gives an empty list rather than throwing`() { + assertThat("not json".toTTPresets()).isEmpty() + assertThat("""{"not":"an array"}""".toTTPresets()).isEmpty() + assertThat("""[{"id":"a"}]""".toTTPresets()).isEmpty() // missing required fields + } + + @Test + fun `a name is omitted rather than written as null`() { + val json = listOf(presets[1]).toJson() + + assertThat(json).doesNotContain("name") + } +} diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/di/PluginsConstraintsModule.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/di/PluginsConstraintsModule.kt index ce614d6af9e3..cbb49da4b359 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/di/PluginsConstraintsModule.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/di/PluginsConstraintsModule.kt @@ -18,7 +18,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import javax.inject.Singleton @Module( @@ -44,5 +45,5 @@ open class PluginsConstraintsModule { @Provides @Singleton - fun providesVersionDefinition(context: Context, signatureVerifierPlugin: SignatureVerifierPlugin): VersionDefinition = VersionDefinition { JSONObject(signatureVerifierPlugin.readInputStream(context.assets.open("definition.json"))) } + fun providesVersionDefinition(context: Context, signatureVerifierPlugin: SignatureVerifierPlugin): VersionDefinition = VersionDefinition { Json.parseToJsonElement(signatureVerifierPlugin.readInputStream(context.assets.open("definition.json"))).jsonObject } } \ No newline at end of file diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt index 088ac8c864f1..e1444382c44d 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt @@ -1,16 +1,25 @@ package app.aaps.plugins.constraints.versionChecker -import app.aaps.core.utils.JsonHelper import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive object AllowedVersions { - fun findByApi(definition: JSONObject, api: Int): String? = JsonHelper.safeGetString(definition, api.toString()) + fun findByApi(definition: JsonObject, api: Int): String? = definition.stringOrNull(api.toString()) - fun findByVersion(definition: JSONObject, version: String): String? = JsonHelper.safeGetString(definition, version) + fun findByVersion(definition: JsonObject, version: String): String? = definition.stringOrNull(version) + + /** + * Missing key -> null, matching what `JsonHelper.safeGetString` did. + * + * `content` rather than a string-only check on purpose: org.json's `getString` coerces a number + * or boolean to its text, so filtering to string primitives would reject values the old reader + * accepted. + */ + private fun JsonObject.stringOrNull(key: String): String? = (this[key] as? JsonPrimitive)?.content fun endDateToMilliseconds(endDate: String): Long? = try { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt index f9e7686ff4e5..00f831310839 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt @@ -17,7 +17,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.plugins.constraints.R import app.aaps.plugins.constraints.versionChecker.keys.VersionCheckerLongKey import dagger.Lazy -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import javax.inject.Inject import javax.inject.Singleton @@ -32,7 +32,7 @@ class VersionCheckerUtilsImpl @Inject constructor( versionDefinition: VersionDefinition ) : VersionCheckerUtils { - var definition: JSONObject = versionDefinition.invoke() + var definition: JsonObject = versionDefinition.invoke() override fun triggerCheckVersion() { val version: String? = AllowedVersions.findByApi(definition, Build.VERSION.SDK_INT) diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/signatureVerifier/VersionCheckerUtilsKtTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/signatureVerifier/VersionCheckerUtilsKtTest.kt index ae0d9574302c..0e0193df8220 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/signatureVerifier/VersionCheckerUtilsKtTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/signatureVerifier/VersionCheckerUtilsKtTest.kt @@ -11,7 +11,8 @@ import app.aaps.plugins.constraints.versionChecker.numericVersionPart import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat import dagger.Lazy -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.anyInt @@ -47,7 +48,7 @@ class VersionCheckerUtilsKtTest : TestBase() { "}" @BeforeEach fun setup() { - val definition = VersionDefinition { JSONObject(generateSupportedVersions()) } + val definition = VersionDefinition { Json.parseToJsonElement(generateSupportedVersions()).jsonObject } versionCheckerUtils = VersionCheckerUtilsImpl(aapsLogger, preferences, rh, config, dateUtil, notificationManager, definition) } diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt index 25136c1df1c2..e446e1eeee6b 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt @@ -1,7 +1,8 @@ package app.aaps.plugins.constraints.versionChecker import com.google.common.truth.Truth.assertThat -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject import org.junit.jupiter.api.Test import java.time.Instant import java.time.LocalDate @@ -27,14 +28,14 @@ class AllowedVersionsTest { @Test fun generateSupportedVersionsTest() { - val definition = JSONObject(generateSupportedVersions()) + val definition = Json.parseToJsonElement(generateSupportedVersions()).jsonObject assertThat(AllowedVersions.findByApi(definition, 0)).isNull() assertThat(AllowedVersions.findByApi(definition, 30)).isEqualTo("3.3.1") } @Test fun findByVersionTest() { - val definition = JSONObject(generateSupportedVersions()) + val definition = Json.parseToJsonElement(generateSupportedVersions()).jsonObject assertThat(AllowedVersions.findByApi(definition, 0)).isNull() assertThat(AllowedVersions.findByApi(definition, 30)).isEqualTo("3.3.1") assertThat(AllowedVersions.findByVersion(definition, "3.2.9")).isNull() @@ -44,7 +45,7 @@ class AllowedVersionsTest { @Suppress("SpellCheckingInspection") @Test fun endDateToMilliseconds() { - val definition = JSONObject(generateSupportedVersions()) + val definition = Json.parseToJsonElement(generateSupportedVersions()).jsonObject val endDate = AllowedVersions.endDateToMilliseconds(AllowedVersions.findByVersion(definition, "3.3.0") ?: "") ?: 0L val dateTime = LocalDate.ofInstant(Instant.ofEpochMilli(endDate), ZoneId.systemDefault()) assertThat(dateTime.year).isEqualTo(2025) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index 980f4944657a..0400c94e1a0c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -262,7 +262,7 @@ class NSClientV3Service : DaggerService() { val collection = response.getString("colName") val docJson = response.getJSONObject("doc") val docString = response.getString("doc") - nsClientRepository.addLog("◄ WS CREATE/UPDATE", collection, docJson) + nsClientRepository.addLog("◄ WS CREATE/UPDATE", collection, docJson.toKotlinxJson()) val srvModified = docJson.getLong("srvModified") // Don't advance the high-water-mark until the initial catch-up load chain // has finished after a (re)connect. Otherwise the Load*Worker chain would From d6c31fa89a5552a68a7f94c09596c89b9f7db38a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 16:50:43 +0200 Subject: [PATCH 072/146] Pump status JSON on kotlinx, driver returns its extended entries --- .../app/aaps/core/interfaces/pump/Pump.kt | 11 +- .../interfaces/pump/PumpStatusProvider.kt | 4 +- .../core/objects/extensions/JSONObjectExt.kt | 24 +++ .../pump/PumpStatusProviderImpl.kt | 65 ++++---- .../pump/PumpWithConcentrationImpl.kt | 4 +- .../pump/PumpStatusProviderImplTest.kt | 140 ++++++++++++++++++ .../nightscout/pump/combov2/ComboV2Plugin.kt | 14 +- .../app/aaps/shared/tests/TestPumpPlugin.kt | 6 + 8 files changed, 223 insertions(+), 45 deletions(-) create mode 100644 implementation/src/test/kotlin/app/aaps/implementation/pump/PumpStatusProviderImplTest.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt index 49c18cf4cd39..97ee0d476c4c 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt @@ -8,7 +8,7 @@ import app.aaps.core.interfaces.pump.actions.CustomAction import app.aaps.core.interfaces.pump.actions.CustomActionType import app.aaps.core.interfaces.queue.CustomCommand import kotlinx.coroutines.flow.StateFlow -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject /** * This interface defines the communication from AAPS-core to pump drivers. @@ -208,12 +208,17 @@ interface Pump { suspend fun cancelExtendedBolus(): PumpEnactResult /** - * Status to be passed to NS. + * Driver specific entries for the "extended" section of the Nightscout pump status. * * This info is displayed when user hover over pump pill in NS. * Except common information every driver can add own info here. + * + * The driver **returns** its entries and the caller merges them. It used to be handed a mutable + * object to write into, which cannot work once that document is an immutable tree - and a return + * value is the clearer contract anyway, since a driver can no longer accidentally remove or + * overwrite the common fields. */ - fun updateExtendedJsonStatus(extendedStatus: JSONObject) {} + fun extendedStatus(): JsonObject = JsonObject(emptyMap()) /** * Manufacturer type. Usually defined by used plugin diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt index a5d92cf852bc..aecb3f3b9204 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.pump -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface PumpStatusProvider { @@ -12,5 +12,5 @@ interface PumpStatusProvider { /** * Generate JSON status of pump sent to the NS */ - suspend fun generatePumpJsonStatus(): JSONObject + suspend fun generatePumpJsonStatus(): JsonObject } diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt index bf72af72df4c..437290eb27a4 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt @@ -7,6 +7,8 @@ import app.aaps.core.keys.interfaces.LongPreferenceKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringNonPreferenceKey import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.put import org.json.JSONObject fun JSONObject.put(key: IntPreferenceKey, preferences: Preferences): JSONObject = @@ -68,3 +70,25 @@ fun JSONObject.putIfThereIsValue(key: String, value: Double?): JSONObject = fun JSONObject.putIfThereIsValue(key: String, value: String?): JSONObject = this.also { if (value != null && value.isNotEmpty()) it.put(key, value) } + +/** + * kotlinx twins of [putIfThereIsValue], with the same rule: skip nulls **and** zeros. + * + * The zero-skipping is not cosmetic. It is what keeps an idle temp basal or extended bolus out of the + * Nightscout device status entirely, rather than reporting a rate of 0 U/h. + */ +fun JsonObjectBuilder.putIfThereIsValue(key: String, value: Int?) { + if (value != null && value != 0) put(key, value) +} + +fun JsonObjectBuilder.putIfThereIsValue(key: String, value: Long?) { + if (value != null && value != 0L) put(key, value) +} + +fun JsonObjectBuilder.putIfThereIsValue(key: String, value: Double?) { + if (value != null && value != 0.0) put(key, value) +} + +fun JsonObjectBuilder.putIfThereIsValue(key: String, value: String?) { + if (value != null && value.isNotEmpty()) put(key, value) +} diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpStatusProviderImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpStatusProviderImpl.kt index 830a36620f27..def73cdaf7c2 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpStatusProviderImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpStatusProviderImpl.kt @@ -12,7 +12,10 @@ import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Translator import app.aaps.core.objects.extensions.putIfThereIsValue import app.aaps.implementation.R -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import javax.inject.Inject import javax.inject.Singleton @@ -79,41 +82,43 @@ class PumpStatusProviderImpl @Inject constructor( /** * Generate JSON status of pump sent to the NS */ - override suspend fun generatePumpJsonStatus(): JSONObject { + override suspend fun generatePumpJsonStatus(): JsonObject { val pump = activePlugin.activePump // do not send data older than 60 minutes - if (dateUtil.isOlderThan(date = pump.lastDataTime.value, minutes = 60)) return JSONObject() + if (dateUtil.isOlderThan(date = pump.lastDataTime.value, minutes = 60)) return JsonObject(emptyMap()) // Do not send any info if there is no running profile - val profile = profileFunction.getProfile() ?: return JSONObject() + val profile = profileFunction.getProfile() ?: return JsonObject(emptyMap()) val expectedPumpState = pumpSync.expectedPumpState() val now = System.currentTimeMillis() val runningMode = persistenceLayer.getRunningModeActiveAt(now) + val profileName = profileFunction.getProfileName() - val pumpJson = JSONObject() - .put("reservoir", pump.reservoirLevel.value.iU(profile.insulinConcentration()).toInt()) - .put("clock", dateUtil.toISOString(now)) - val battery = JSONObject().putIfThereIsValue("percent", pump.batteryLevel.value) - val status = JSONObject() - .put("status", translator.translate(runningMode.mode)) - .put("timestamp", dateUtil.toISOString(pump.lastDataTime.value)) - val extended = JSONObject() - .put("Version", config.VERSION_NAME + "-" + config.BUILD_VERSION) - .putIfThereIsValue("LastBolus", dateUtil.dateAndTimeStringNullable(pump.lastBolusTime.value)) - .putIfThereIsValue("LastBolusAmount", pump.lastBolusAmount.value?.iU(profile.insulinConcentration())) - .putIfThereIsValue("TempBasalAbsoluteRate", expectedPumpState.temporaryBasal?.convertedToAbsolute(now, profile)) - .putIfThereIsValue("TempBasalStart", dateUtil.dateAndTimeStringNullable(expectedPumpState.temporaryBasal?.timestamp)) - .putIfThereIsValue("TempBasalRemaining", expectedPumpState.temporaryBasal?.plannedRemainingMinutes) - .putIfThereIsValue("ExtendedBolusAbsoluteRate", expectedPumpState.extendedBolus?.rate) - .putIfThereIsValue("ExtendedBolusStart", dateUtil.dateAndTimeStringNullable(expectedPumpState.extendedBolus?.timestamp)) - .putIfThereIsValue("ExtendedBolusRemaining", expectedPumpState.extendedBolus?.plannedRemainingMinutes) - .putIfThereIsValue("BaseBasalRate", pump.baseBasalRate.iU(profile.insulinConcentration(), true)) - .put("ActiveProfile", profileFunction.getProfileName()) - - // grab more values from pump if provided - pump.updateExtendedJsonStatus(extended) - return pumpJson - .put("battery", battery) - .put("status", status) - .put("extended", extended) + return buildJsonObject { + put("reservoir", pump.reservoirLevel.value.iU(profile.insulinConcentration()).toInt()) + put("clock", dateUtil.toISOString(now)) + putJsonObject("battery") { + putIfThereIsValue("percent", pump.batteryLevel.value) + } + putJsonObject("status") { + put("status", translator.translate(runningMode.mode)) + put("timestamp", dateUtil.toISOString(pump.lastDataTime.value)) + } + putJsonObject("extended") { + put("Version", config.VERSION_NAME + "-" + config.BUILD_VERSION) + putIfThereIsValue("LastBolus", dateUtil.dateAndTimeStringNullable(pump.lastBolusTime.value)) + putIfThereIsValue("LastBolusAmount", pump.lastBolusAmount.value?.iU(profile.insulinConcentration())) + putIfThereIsValue("TempBasalAbsoluteRate", expectedPumpState.temporaryBasal?.convertedToAbsolute(now, profile)) + putIfThereIsValue("TempBasalStart", dateUtil.dateAndTimeStringNullable(expectedPumpState.temporaryBasal?.timestamp)) + putIfThereIsValue("TempBasalRemaining", expectedPumpState.temporaryBasal?.plannedRemainingMinutes) + putIfThereIsValue("ExtendedBolusAbsoluteRate", expectedPumpState.extendedBolus?.rate) + putIfThereIsValue("ExtendedBolusStart", dateUtil.dateAndTimeStringNullable(expectedPumpState.extendedBolus?.timestamp)) + putIfThereIsValue("ExtendedBolusRemaining", expectedPumpState.extendedBolus?.plannedRemainingMinutes) + putIfThereIsValue("BaseBasalRate", pump.baseBasalRate.iU(profile.insulinConcentration(), true)) + put("ActiveProfile", profileName) + // Driver specific entries last, matching the previous order - the driver used to be + // handed the finished object and could still add to it. + pump.extendedStatus().forEach { (key, value) -> put(key, value) } + } + } } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpWithConcentrationImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpWithConcentrationImpl.kt index 9010e10ed3fc..9b0b2771feeb 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpWithConcentrationImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpWithConcentrationImpl.kt @@ -27,7 +27,7 @@ import app.aaps.core.interfaces.queue.CustomCommand import app.aaps.core.interfaces.utils.Round import app.aaps.core.objects.constraints.ConstraintObject import kotlinx.coroutines.flow.StateFlow -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject import javax.inject.Inject import javax.inject.Provider @@ -63,7 +63,7 @@ class PumpWithConcentrationImpl @Inject constructor( override val batteryLevel: StateFlow get() = activePumpInternal.batteryLevel override suspend fun cancelTempBasal(enforceNew: Boolean): PumpEnactResult = activePumpInternal.cancelTempBasal(enforceNew) override suspend fun cancelExtendedBolus(): PumpEnactResult = activePumpInternal.cancelExtendedBolus() - override fun updateExtendedJsonStatus(extendedStatus: JSONObject) = activePumpInternal.updateExtendedJsonStatus(extendedStatus) + override fun extendedStatus(): JsonObject = activePumpInternal.extendedStatus() override fun manufacturer(): ManufacturerType = activePumpInternal.manufacturer() override fun model(): PumpType = activePumpInternal.model() override fun serialNumber(): String = activePumpInternal.serialNumber() diff --git a/implementation/src/test/kotlin/app/aaps/implementation/pump/PumpStatusProviderImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/pump/PumpStatusProviderImplTest.kt new file mode 100644 index 000000000000..b844188515d1 --- /dev/null +++ b/implementation/src/test/kotlin/app/aaps/implementation/pump/PumpStatusProviderImplTest.kt @@ -0,0 +1,140 @@ +package app.aaps.implementation.pump + +import app.aaps.core.data.model.RM +import app.aaps.core.interfaces.db.PersistenceLayer +import app.aaps.core.interfaces.pump.PumpSync +import app.aaps.core.interfaces.utils.Translator +import app.aaps.shared.tests.TestBaseWithProfile +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.json.JSONObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +/** + * Pins the Nightscout device-status payload built by [PumpStatusProviderImpl]. + * + * There was no coverage here at all, which is thin for something uploaded to Nightscout on every + * loop: a field silently appearing, vanishing or changing shape is invisible locally and only shows + * up as wrong data on the server. + * + * Two behaviours matter most and are easy to break by accident. Empty values are **omitted** rather + * than sent as zero - `putIfThereIsValue` skips nulls *and* zeros, which is what keeps idle temp-basal + * and extended-bolus fields out of the payload. And the active pump gets to contribute its own + * entries to `extended`, which is the seam the driver hook hangs on. + */ +class PumpStatusProviderImplTest : TestBaseWithProfile() { + + @Mock lateinit var pumpSync: PumpSync + @Mock lateinit var persistenceLayer: PersistenceLayer + @Mock lateinit var translator: Translator + + private lateinit var sut: PumpStatusProviderImpl + + @BeforeEach + fun prepare() = runTest { + sut = PumpStatusProviderImpl( + activePlugin, pumpSync, profileFunction, persistenceLayer, + rh, dateUtil, decimalFormatter, translator, config + ) + whenever(activePlugin.activePump).thenReturn(testPumpPlugin) + whenever(profileFunction.getProfile()).thenReturn(effectiveProfile) + whenever(profileFunction.getProfileName()).thenReturn("SomeProfile") + whenever(pumpSync.expectedPumpState()).thenReturn(PumpSync.PumpState(null, null, null, null, "1")) + whenever(persistenceLayer.getRunningModeActiveAt(any())).thenReturn(RM(timestamp = 0, mode = RM.Mode.OPEN_LOOP, duration = 0L)) + whenever(translator.translate(any())).thenReturn("Open Loop") + // Real clock, not dateUtil.now(): the base stubs now() to a fixed value while isOlderThan is a + // spy that still reads the system clock, so a stubbed timestamp always looks an hour stale. + testPumpPlugin.lastData = System.currentTimeMillis() + } + + /** + * The assertions below were written against the previous `org.json` implementation and are kept + * word for word across the move to kotlinx. Re-parsing the produced document with `org.json` is + * what makes that possible, and it is also the stronger statement: not just "the new code has the + * same fields", but "the bytes we upload still parse into the same document". + */ + private suspend fun status(): JSONObject = JSONObject(sut.generatePumpJsonStatus().toString()) + + @Test + fun `carries the top level shape Nightscout expects`() = runTest { + val json = status() + + assertThat(json.has("reservoir")).isTrue() + assertThat(json.has("clock")).isTrue() + assertThat(json.has("status")).isTrue() + assertThat(json.has("extended")).isTrue() + assertThat(json.getJSONObject("status").getString("status")).isEqualTo("Open Loop") + assertThat(json.getJSONObject("extended").has("Version")).isTrue() + assertThat(json.getJSONObject("extended").getString("ActiveProfile")).isEqualTo("SomeProfile") + } + + /** + * Stale pump data must not be uploaded at all - an hour-old reservoir reading presented as current + * is worse than no reading. + */ + @Test + fun `data older than an hour yields an empty document`() = runTest { + // Real clock, not dateUtil.now(): the base stubs now() to a fixed value while isOlderThan is a + // spy that still reads the system clock, so a stubbed timestamp always looks an hour stale. + testPumpPlugin.lastData = System.currentTimeMillis() - 61 * 60 * 1000L + + assertThat(status().length()).isEqualTo(0) + } + + @Test + fun `no running profile yields an empty document`() = runTest { + whenever(profileFunction.getProfile()).thenReturn(null) + + assertThat(status().length()).isEqualTo(0) + } + + /** + * The zero-skipping in `putIfThereIsValue`. With no temp basal or extended bolus running, those + * keys must be absent rather than present-and-zero, or Nightscout shows a 0 U/h temp basal. + */ + @Test + fun `absent values are omitted rather than sent as zero`() = runTest { + val extended = status().getJSONObject("extended") + + assertThat(extended.has("TempBasalAbsoluteRate")).isFalse() + assertThat(extended.has("TempBasalRemaining")).isFalse() + assertThat(extended.has("ExtendedBolusAbsoluteRate")).isFalse() + assertThat(extended.has("ExtendedBolusRemaining")).isFalse() + assertThat(extended.has("LastBolusAmount")).isFalse() + } + + /** A battery level of null must not appear as 0%. */ + @Test + fun `a missing battery level is omitted`() = runTest { + assertThat(status().getJSONObject("battery").has("percent")).isFalse() + } + + /** + * The driver extension seam: whatever the active pump contributes has to reach `extended`. This is + * the only thing [app.aaps.core.interfaces.pump.Pump.extendedStatus] exists for, and Combo V2 uses + * it to report alert codes. + */ + @Test + fun `entries contributed by the pump reach the extended section`() = runTest { + testPumpPlugin.extendedStatusExtras = buildJsonObject { put("WarningCode", "W73") } + + assertThat(status().getJSONObject("extended").getString("WarningCode")).isEqualTo("W73") + } + + /** + * Combo V2 contributes an `Int` alert code, so the value has to stay a JSON number. Flattening + * driver entries to strings would silently change what Nightscout receives. + */ + @Test + fun `a numeric entry from the pump stays a number`() = runTest { + testPumpPlugin.extendedStatusExtras = buildJsonObject { put("WarningCode", 73) } + + assertThat(status().getJSONObject("extended").get("WarningCode")).isEqualTo(73) + } +} diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index e6d33e6f639f..3d52198a2e31 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -92,8 +92,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import org.joda.time.DateTime -import org.json.JSONObject import java.util.Locale import javax.inject.Inject import javax.inject.Provider @@ -1224,14 +1226,10 @@ class ComboV2Plugin @Inject constructor( override suspend fun cancelExtendedBolus(): PumpEnactResult = createFailurePumpEnactResult(R.string.combov2_extended_bolus_not_supported) - override fun updateExtendedJsonStatus(extendedStatus: JSONObject) { + override fun extendedStatus(): JsonObject = buildJsonObject { when (val alert = lastComboAlert) { - is AlertScreenContent.Warning -> - extendedStatus.put("WarningCode", alert.code) - - is AlertScreenContent.Error -> - extendedStatus.put("ErrorCode", alert.code) - + is AlertScreenContent.Warning -> put("WarningCode", alert.code) + is AlertScreenContent.Error -> put("ErrorCode", alert.code) else -> Unit } } diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestPumpPlugin.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestPumpPlugin.kt index f424b72b446a..1cd917b67d02 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestPumpPlugin.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestPumpPlugin.kt @@ -17,6 +17,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.implementation.pump.PumpEnactResultObject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.json.JsonObject @Suppress("MemberVisibilityCanBePrivate") class TestPumpPlugin(val rh: ResourceHelper) : PumpWithConcentration { @@ -94,5 +95,10 @@ class TestPumpPlugin(val rh: ResourceHelper) : PumpWithConcentration { override suspend fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { /* not needed */ } + /** Entries this fake pump contributes to the Nightscout "extended" section. */ + var extendedStatusExtras: JsonObject = JsonObject(emptyMap()) + + override fun extendedStatus(): JsonObject = extendedStatusExtras + override fun selectedActivePump(): Pump = this } \ No newline at end of file From 986f790e140caba939a05ad160c00a1378ea56e1 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 16:50:52 +0200 Subject: [PATCH 073/146] Remove NSSettingsStatus, which never received any data NSSettingsStatus was fed from the V1 /api/v1/status.json document. The NSClient V3 rewrite dropped that call, and the V3 status response does not carry "settings" or "extendedSettings" at all, so handleNewData had no callers left and the backing field stayed null forever. Everything reading it therefore always got the fallback: - the log report mailed to developers always said "Nightscout version: UNKNOWN". It now uses NsClient.detectedNsVersion(), the same source the About dialog already shows. - the AAPSClient pump pill always used the built-in thresholds. Those are now named constants next to the code that reads them. - the "Copy NS settings (if exists)?" button read eight values that were always null, so all eight writes were skipped. The user confirmed a dialog, nothing changed, and only a history entry was written. The button and its dialog are gone. Action.NS_SETTINGS_COPIED and its converters stay so existing history rows still render. Nothing writes it any more. --- .../interfaces/nsclient/NSSettingsStatus.kt | 12 - core/ui/src/main/res/values/strings.xml | 1 - .../maintenance/MaintenanceImpl.kt | 6 +- .../maintenance/MaintenanceImplTest.kt | 6 +- .../app/aaps/plugins/sync/di/SyncModule.kt | 3 - .../nsclientV3/data/NSSettingsStatusImpl.kt | 208 ------------------ .../ui/compose/manageSheet/ManageViewModel.kt | 25 --- .../compose/overview/OverviewDataCacheImpl.kt | 33 ++- .../compose/overview/OverviewScreenSplit.kt | 1 - .../compose/overview/OverviewScreenStacked.kt | 1 - .../compose/overview/OverviewScreenTablet.kt | 1 - .../compose/overview/OverviewStatusSection.kt | 38 +--- .../overview/OverviewStatusSectionPreviews.kt | 2 - .../manageSheet/ManageViewModelTest.kt | 8 +- .../StatusLightsSettingsContentTest.kt | 3 +- 15 files changed, 33 insertions(+), 315 deletions(-) delete mode 100644 core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSSettingsStatus.kt delete mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSSettingsStatusImpl.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSSettingsStatus.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSSettingsStatus.kt deleted file mode 100644 index eee956301c22..000000000000 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSSettingsStatus.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.aaps.core.interfaces.nsclient - -import org.json.JSONObject - -interface NSSettingsStatus { - - fun handleNewData(status: JSONObject) - fun getVersion(): String - fun extendedPumpSettings(setting: String?): Double - fun pumpExtendedSettingsFields(): String - fun getExtendedWarnValue(plugin: String, property: String): Double? -} \ No newline at end of file diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index bc9525890e85..109da3f8a4e5 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -70,7 +70,6 @@ Confirm PIN %1$s%2$s (%3$s – %4$s) Status lights - Copy NS settings (if exists)? Insulin BG Source Smoothing diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/MaintenanceImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/MaintenanceImpl.kt index 20f8ed0ab8a7..c61182d901d9 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/MaintenanceImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/MaintenanceImpl.kt @@ -10,8 +10,8 @@ import app.aaps.core.interfaces.logging.LoggerUtils import app.aaps.core.interfaces.maintenance.ExportResult import app.aaps.core.interfaces.maintenance.FileListProvider import app.aaps.core.interfaces.maintenance.Maintenance -import app.aaps.core.interfaces.nsclient.NSSettingsStatus import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.sync.NsClient import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.IntKey import app.aaps.core.keys.StringKey @@ -35,7 +35,7 @@ class MaintenanceImpl @Inject constructor( private val context: Context, private val rh: ResourceHelper, private val preferences: Preferences, - private val nsSettingsStatus: NSSettingsStatus, + private val nsClient: NsClient, private val aapsLogger: AAPSLogger, private val config: Config, private val fileListProvider: FileListProvider, @@ -193,7 +193,7 @@ class MaintenanceImpl @Inject constructor( builder.append("Build: " + config.BUILD_VERSION + System.lineSeparator()) builder.append("Remote: " + config.REMOTE + System.lineSeparator()) builder.append("Flavor: " + config.FLAVOR + config.BUILD_TYPE + System.lineSeparator()) - builder.append(rh.gs(app.aaps.core.ui.R.string.configbuilder_nightscoutversion_label) + " " + nsSettingsStatus.getVersion() + System.lineSeparator()) + builder.append(rh.gs(app.aaps.core.ui.R.string.configbuilder_nightscoutversion_label) + " " + (nsClient.detectedNsVersion() ?: "UNKNOWN") + System.lineSeparator()) if (config.isEngineeringMode()) builder.append(rh.gs(app.aaps.core.ui.R.string.engineering_mode_enabled)) val body = builder.toString() aapsLogger.debug("sending email to $recipient with subject $subject") diff --git a/implementation/src/test/kotlin/app/aaps/implementation/maintenance/MaintenanceImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/maintenance/MaintenanceImplTest.kt index 540fe92a72df..b3afe5392433 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/maintenance/MaintenanceImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/maintenance/MaintenanceImplTest.kt @@ -2,7 +2,7 @@ package app.aaps.implementation.maintenance import app.aaps.core.interfaces.logging.LoggerUtils import app.aaps.core.interfaces.maintenance.FileListProvider -import app.aaps.core.interfaces.nsclient.NSSettingsStatus +import app.aaps.core.interfaces.sync.NsClient import app.aaps.implementation.maintenance.cloud.CloudStorageManager import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat @@ -13,7 +13,7 @@ import org.mockito.kotlin.whenever class MaintenanceImplTest : TestBaseWithProfile() { - @Mock lateinit var nsSettingsStatus: NSSettingsStatus + @Mock lateinit var nsClient: NsClient @Mock lateinit var loggerUtils: LoggerUtils @Mock lateinit var fileListProvider: FileListProvider @Mock lateinit var cloudStorageManager: CloudStorageManager @@ -22,7 +22,7 @@ class MaintenanceImplTest : TestBaseWithProfile() { @BeforeEach fun mock() { - sut = MaintenanceImpl(context, rh, preferences, nsSettingsStatus, aapsLogger, config, fileListProvider, loggerUtils, cloudStorageManager) + sut = MaintenanceImpl(context, rh, preferences, nsClient, aapsLogger, config, fileListProvider, loggerUtils, cloudStorageManager) whenever(loggerUtils.suffix).thenReturn(".log.zip") whenever(loggerUtils.logDirectory).thenReturn("src/test/assets/logger") } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt index 9c46246bbf38..edbdffd1f51c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.work.WorkManager import app.aaps.core.interfaces.clientcontrol.ClientControlActionDispatcher import app.aaps.core.interfaces.nsclient.NSClientRepository -import app.aaps.core.interfaces.nsclient.NSSettingsStatus import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.nsclient.StoreDataForDb import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator @@ -17,7 +16,6 @@ import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.nsclientV3.StoreDataForDbImpl import app.aaps.plugins.sync.nsclientV3.clientcontrol.ClientControlRoundTrip import app.aaps.plugins.sync.nsclientV3.compose.NSClientRepositoryImpl -import app.aaps.plugins.sync.nsclientV3.data.NSSettingsStatusImpl import app.aaps.plugins.sync.nsclientV3.data.ProcessedDeviceStatusDataImpl import app.aaps.plugins.sync.nsclientV3.services.NSClientV3Service import app.aaps.plugins.sync.smsCommunicator.SmsCommunicatorPlugin @@ -66,7 +64,6 @@ abstract class SyncModule { interface Binding { @Binds fun bindProcessedDeviceStatusData(processedDeviceStatusDataImpl: ProcessedDeviceStatusDataImpl): ProcessedDeviceStatusData - @Binds fun bindNSSettingsStatus(nsSettingsStatusImpl: NSSettingsStatusImpl): NSSettingsStatus @Binds fun bindDataSyncSelectorXdripInterface(dataSyncSelectorXdripImpl: DataSyncSelectorXdripImpl): DataSyncSelectorXdrip @Binds fun bindStoreDataForDb(storeDataForDbImpl: StoreDataForDbImpl): StoreDataForDb @Binds fun bindSmsCommunicator(smsCommunicatorPlugin: SmsCommunicatorPlugin): SmsCommunicator diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSSettingsStatusImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSSettingsStatusImpl.kt deleted file mode 100644 index 7fee2ecd9cb5..000000000000 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSSettingsStatusImpl.kt +++ /dev/null @@ -1,208 +0,0 @@ -@file:Suppress("SpellCheckingInspection") - -package app.aaps.plugins.sync.nsclientV3.data - -import app.aaps.core.interfaces.configuration.Config -import app.aaps.core.interfaces.logging.AAPSLogger -import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.notifications.NotificationId -import app.aaps.core.interfaces.notifications.NotificationLevel -import app.aaps.core.interfaces.notifications.NotificationManager -import app.aaps.core.interfaces.nsclient.NSSettingsStatus -import app.aaps.core.utils.JsonHelper -import app.aaps.plugins.sync.R -import org.json.JSONException -import org.json.JSONObject -import javax.inject.Inject -import javax.inject.Singleton - -/* - { - "status": "ok", - "name": "Nightscout", - "version": "0.10.0-dev-20170423", - "versionNum": 1000, - "serverTime": "2017-06-12T07:46:56.006Z", - "apiEnabled": true, - "careportalEnabled": true, - "boluscalcEnabled": true, - "head": "96ee154", - "settings": { - "units": "mmol", - "timeFormat": 24, - "nightMode": false, - "editMode": true, - "showRawbg": "always", - "customTitle": "Bara's CGM", - "theme": "colors", - "alarmUrgentHigh": true, - "alarmUrgentHighMins": [30, 60, 90, 120], - "alarmHigh": true, - "alarmHighMins": [30, 60, 90, 120], - "alarmLow": true, - "alarmLowMins": [15, 30, 45, 60], - "alarmUrgentLow": true, - "alarmUrgentLowMins": [15, 30, 45], - "alarmUrgentMins": [30, 60, 90, 120], - "alarmWarnMins": [30, 60, 90, 120], - "alarmTimeagoWarn": true, - "alarmTimeagoWarnMins": 15, - "alarmTimeagoUrgent": true, - "alarmTimeagoUrgentMins": 30, - "language": "cs", - "scaleY": "linear", - "showPlugins": "careportal boluscalc food bwp cage sage iage iob cob basal ar2 delta direction upbat rawbg", - "showForecast": "ar2", - "focusHours": 3, - "heartbeat": 60, - "baseURL": "http:\/\/xxxxxxxxxxxx", - "authDefaultRoles": "readable", - "thresholds": { - "bgHigh": 252, - "bgTargetTop": 180, - "bgTargetBottom": 72, - "bgLow": 71 - }, - "DEFAULT_FEATURES": ["bgnow", "delta", "direction", "timeago", "devicestatus", "upbat", "errorcodes", "profile"], - "alarmTypes": ["predict"], - "enable": ["careportal", "boluscalc", "food", "bwp", "cage", "sage", "iage", "iob", "cob", "basal", "ar2", "rawbg", "pushover", "bgi", "pump", "openaps", "pushover", "treatmentnotify", "bgnow", "delta", "direction", "timeago", "devicestatus", "upbat", "profile", "ar2"] - }, - "extendedSettings": { - "pump": { - "fields": "reservoir battery clock", - "urgentBattP": 26, - "warnBattP": 51 - }, - "openaps": { - "enableAlerts": true - }, - "cage": { - "alerts": true, - "display": "days", - "urgent": 96, - "warn": 72 - }, - "sage": { - "alerts": true, - "urgent": 336, - "warn": 168 - }, - "iage": { - "alerts": true, - "urgent": 150, - "warn": 120 - }, - "basal": { - "render": "default" - }, - "profile": { - "history": true, - "multiple": true - }, - "devicestatus": { - "advanced": true - } - }, - "activeProfile": "2016 +30%" - } - */ -@Singleton -class NSSettingsStatusImpl @Inject constructor( - private val aapsLogger: AAPSLogger, - private val notificationManager: NotificationManager, - private val config: Config -) : NSSettingsStatus { - - // ***** PUMP STATUS ****** - private var data: JSONObject? = null - - /* Other received data to 2016/02/10 - { - status: 'ok' - , name: env.name - , version: env.version - , versionNum: versionNum (for ver 1.2.3 contains 10203) - , serverTime: new Date().toISOString() - , apiEnabled: apiEnabled - , careportalEnabled: apiEnabled && env.settings.enable.indexOf('careportal') > -1 - , boluscalcEnabled: apiEnabled && env.settings.enable.indexOf('boluscalc') > -1 - , head: env.head - , settings: env.settings - , extendedSettings: ctx.plugins && ctx.plugins.extendedClientSettings ? ctx.plugins.extendedClientSettings(env.extendedSettings) : {} - , activeProfile ..... calculated from treatments or missing - } - */ - - override fun handleNewData(status: JSONObject) { - data = status - aapsLogger.debug(LTag.NSCLIENT, "Got versions: Nightscout: ${getVersion()}") - if (getVersionNum() < config.SUPPORTED_NS_VERSION) { - notificationManager.post(NotificationId.OLD_NS, R.string.unsupported_ns_version, level = NotificationLevel.NORMAL) - } else { - notificationManager.dismiss(NotificationId.OLD_NS) - } - data = status - aapsLogger.debug(LTag.NSCLIENT, "Received status: $status") - } - - override fun getVersion(): String = - JsonHelper.safeGetStringAllowNull(data, "version", null) ?: "UNKNOWN" - - private fun getVersionNum(): Int = - JsonHelper.safeGetInt(data, "versionNum") - - private fun getSettings() = - JsonHelper.safeGetJSONObject(data, "settings", null) - - private fun getExtendedSettings(): JSONObject? = - JsonHelper.safeGetJSONObject(data, "extendedSettings", null) - - // valid property is "warn" or "urgent" - // plugins "iage" "sage" "cage" "pbage" - override fun getExtendedWarnValue(plugin: String, property: String): Double? { - val extendedSettings = getExtendedSettings() ?: return null - val pluginJson = extendedSettings.optJSONObject(plugin) ?: return null - return try { - pluginJson.getDouble(property) - } catch (_: Exception) { - null - } - } - - /* - , warnClock: sbx.extendedSettings.warnClock || 30 - , urgentClock: sbx.extendedSettings.urgentClock || 60 - , warnRes: sbx.extendedSettings.warnRes || 10 - , urgentRes: sbx.extendedSettings.urgentRes || 5 - , warnBattV: sbx.extendedSettings.warnBattV || 1.35 - , urgentBattV: sbx.extendedSettings.urgentBattV || 1.3 - , warnBattP: sbx.extendedSettings.warnBattP || 30 - , urgentBattP: sbx.extendedSettings.urgentBattP || 20 - , enableAlerts: sbx.extendedSettings.enableAlerts || false - */ - override fun extendedPumpSettings(setting: String?): Double { - try { - val pump = extendedPumpSettings() - return when (setting) { - "warnClock" -> JsonHelper.safeGetDouble(pump, setting, 30.0) - "urgentClock" -> JsonHelper.safeGetDouble(pump, setting, 60.0) - "warnRes" -> JsonHelper.safeGetDouble(pump, setting, 10.0) - "urgentRes" -> JsonHelper.safeGetDouble(pump, setting, 5.0) - "warnBattV" -> JsonHelper.safeGetDouble(pump, setting, 1.35) - "urgentBattV" -> JsonHelper.safeGetDouble(pump, setting, 1.3) - "warnBattP" -> JsonHelper.safeGetDouble(pump, setting, 30.0) - "urgentBattP" -> JsonHelper.safeGetDouble(pump, setting, 20.0) - else -> 0.0 - } - } catch (e: JSONException) { - aapsLogger.error("Unhandled exception", e) - } - return 0.0 - } - - private fun extendedPumpSettings(): JSONObject? = - JsonHelper.safeGetJSONObject(getExtendedSettings(), "pump", null) - - override fun pumpExtendedSettingsFields(): String = - JsonHelper.safeGetString(extendedPumpSettings(), "fields", "") -} \ No newline at end of file diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt index 31c1253c9101..da402f448348 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope import app.aaps.core.data.model.EB import app.aaps.core.data.model.RM import app.aaps.core.data.model.TB -import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources import app.aaps.core.data.ui.ConfirmationLine import app.aaps.core.interfaces.aps.Loop @@ -19,8 +18,6 @@ import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.db.ProcessedTbrEbData import app.aaps.core.interfaces.di.ApplicationScope -import app.aaps.core.interfaces.logging.UserEntryLogger -import app.aaps.core.interfaces.nsclient.NSSettingsStatus import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.profile.ProfileFunction @@ -33,7 +30,6 @@ import app.aaps.core.interfaces.rx.events.EventShowDialog import app.aaps.core.interfaces.sync.NsClient import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.BooleanKey -import app.aaps.core.keys.IntKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.objects.extensions.toStringMedium @@ -68,10 +64,8 @@ class ManageViewModel @Inject constructor( private val config: Config, private val processedTbrEbData: ProcessedTbrEbData, private val persistenceLayer: PersistenceLayer, - private val uel: UserEntryLogger, private val rxBus: RxBus, private val dateUtil: DateUtil, - private val nsSettingStatus: NSSettingsStatus, private val preferences: Preferences, private val batchExecutor: BatchExecutor, private val nsClient: NsClient, @@ -250,23 +244,4 @@ class ManageViewModel @Inject constructor( activePlugin.activePump.executeCustomAction(actionType) } - fun copyStatusLightsFromNightscout() { - val cageWarn = nsSettingStatus.getExtendedWarnValue("cage", "warn")?.toInt() - val cageCritical = nsSettingStatus.getExtendedWarnValue("cage", "urgent")?.toInt() - val iageWarn = nsSettingStatus.getExtendedWarnValue("iage", "warn")?.toInt() - val iageCritical = nsSettingStatus.getExtendedWarnValue("iage", "urgent")?.toInt() - val sageWarn = nsSettingStatus.getExtendedWarnValue("sage", "warn")?.toInt() - val sageCritical = nsSettingStatus.getExtendedWarnValue("sage", "urgent")?.toInt() - val bageWarn = nsSettingStatus.getExtendedWarnValue("bage", "warn")?.toInt() - val bageCritical = nsSettingStatus.getExtendedWarnValue("bage", "urgent")?.toInt() - cageWarn?.let { preferences.put(IntKey.OverviewCageWarning, it) } - cageCritical?.let { preferences.put(IntKey.OverviewCageCritical, it) } - iageWarn?.let { preferences.put(IntKey.OverviewIageWarning, it) } - iageCritical?.let { preferences.put(IntKey.OverviewIageCritical, it) } - sageWarn?.let { preferences.put(IntKey.OverviewSageWarning, it) } - sageCritical?.let { preferences.put(IntKey.OverviewSageCritical, it) } - bageWarn?.let { preferences.put(IntKey.OverviewBageWarning, it) } - bageCritical?.let { preferences.put(IntKey.OverviewBageCritical, it) } - uel.log(Action.NS_SETTINGS_COPIED, Sources.NSClient) - } } \ No newline at end of file diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt index ca892eed5c63..30f16961e31d 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt @@ -23,7 +23,6 @@ import app.aaps.core.interfaces.iob.GlucoseStatusProvider import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.nsclient.NSSettingsStatus import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.overview.graph.AapsClientLevel import app.aaps.core.interfaces.overview.graph.AapsClientStatusData @@ -123,6 +122,19 @@ import kotlin.math.min * MIGRATION NOTE: This coexists with OverviewDataImpl during migration. * Workers populate graph data. After migration complete, OverviewDataImpl will be deleted. */ + +// Thresholds for the pump pill shown on an AAPSClient. These are the Nightscout defaults. They used to +// be read from the server's "extendedSettings" through NSSettingsStatus, but the NSClient V3 API does +// not return that section, so the defaults were the only value ever used. They are stated here now. +private const val WARN_CLOCK_MINUTES = 30L +private const val URGENT_CLOCK_MINUTES = 60L +private const val WARN_RESERVOIR = 10.0 +private const val URGENT_RESERVOIR = 5.0 +private const val WARN_BATTERY_PERCENT = 30.0 +private const val URGENT_BATTERY_PERCENT = 20.0 +private const val WARN_BATTERY_VOLTAGE = 1.35 +private const val URGENT_BATTERY_VOLTAGE = 1.3 + @OptIn(FlowPreview::class) class OverviewDataCacheImpl @AssistedInject constructor( private val aapsLogger: AAPSLogger, @@ -137,7 +149,6 @@ class OverviewDataCacheImpl @AssistedInject constructor( private val loop: Loop, private val config: Config, private val processedDeviceStatusData: ProcessedDeviceStatusData, - private val nsSettingsStatus: NSSettingsStatus, private val rxBus: RxBus, private val activePlugin: ActivePlugin, private val decimalFormatter: DecimalFormatter, @@ -948,15 +959,15 @@ class OverviewDataCacheImpl @AssistedInject constructor( val now = dateUtil.now() val pumpItem = processedDeviceStatusData.pumpData?.let { pumpData -> val level = when { - pumpData.clock + nsSettingsStatus.extendedPumpSettings("urgentClock") * 60 * 1000L < now -> AapsClientLevel.URGENT - pumpData.reservoir < nsSettingsStatus.extendedPumpSettings("urgentRes") -> AapsClientLevel.URGENT - pumpData.isPercent && pumpData.percent < nsSettingsStatus.extendedPumpSettings("urgentBattP") -> AapsClientLevel.URGENT - !pumpData.isPercent && pumpData.voltage > 0 && pumpData.voltage < nsSettingsStatus.extendedPumpSettings("urgentBattV") -> AapsClientLevel.URGENT - pumpData.clock + nsSettingsStatus.extendedPumpSettings("warnClock") * 60 * 1000L < now -> AapsClientLevel.WARN - pumpData.reservoir < nsSettingsStatus.extendedPumpSettings("warnRes") -> AapsClientLevel.WARN - pumpData.isPercent && pumpData.percent < nsSettingsStatus.extendedPumpSettings("warnBattP") -> AapsClientLevel.WARN - !pumpData.isPercent && pumpData.voltage > 0 && pumpData.voltage < nsSettingsStatus.extendedPumpSettings("warnBattV") -> AapsClientLevel.WARN - else -> AapsClientLevel.INFO + pumpData.clock + URGENT_CLOCK_MINUTES * 60 * 1000L < now -> AapsClientLevel.URGENT + pumpData.reservoir < URGENT_RESERVOIR -> AapsClientLevel.URGENT + pumpData.isPercent && pumpData.percent < URGENT_BATTERY_PERCENT -> AapsClientLevel.URGENT + !pumpData.isPercent && pumpData.voltage > 0 && pumpData.voltage < URGENT_BATTERY_VOLTAGE -> AapsClientLevel.URGENT + pumpData.clock + WARN_CLOCK_MINUTES * 60 * 1000L < now -> AapsClientLevel.WARN + pumpData.reservoir < WARN_RESERVOIR -> AapsClientLevel.WARN + pumpData.isPercent && pumpData.percent < WARN_BATTERY_PERCENT -> AapsClientLevel.WARN + !pumpData.isPercent && pumpData.voltage > 0 && pumpData.voltage < WARN_BATTERY_VOLTAGE -> AapsClientLevel.WARN + else -> AapsClientLevel.INFO } // Format: "75% 3 min ago" (running mode excluded — already shown in RunningMode chip) val value = buildString { diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenSplit.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenSplit.kt index 6166ccc8b329..c280c0d8f26f 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenSplit.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenSplit.kt @@ -169,7 +169,6 @@ fun OverviewScreenSplit( commandsAllowed = commandsAllowed, onNavigate = onNavigate, statusLightsDef = statusLightsDef, - onCopyFromNightscout = { manageViewModel.copyStatusLightsFromNightscout() }, expanded = statusExpanded, onExpandedChange = { statusExpanded = it } ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenStacked.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenStacked.kt index 65f13745860b..9489c8296341 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenStacked.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenStacked.kt @@ -149,7 +149,6 @@ fun OverviewScreenStacked( commandsAllowed = commandsAllowed, onNavigate = onNavigate, statusLightsDef = statusLightsDef, - onCopyFromNightscout = { manageViewModel.copyStatusLightsFromNightscout() }, expanded = statusExpanded, onExpandedChange = { statusExpanded = it } ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenTablet.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenTablet.kt index 984c4755a304..60e95573fcb0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenTablet.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewScreenTablet.kt @@ -180,7 +180,6 @@ fun OverviewScreenTablet( commandsAllowed = commandsAllowed, onNavigate = onNavigate, statusLightsDef = statusLightsDef, - onCopyFromNightscout = { manageViewModel.copyStatusLightsFromNightscout() }, expanded = statusExpanded, onExpandedChange = { statusExpanded = it } ) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSection.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSection.kt index 8eb47bec5ef0..19502627cc1e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSection.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSection.kt @@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -47,7 +46,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.navigation.ElementType -import app.aaps.core.ui.compose.dialogs.OkCancelDialog import app.aaps.core.ui.compose.navigation.NavigationRequest import app.aaps.core.ui.compose.preference.PreferenceSheetContent import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -73,7 +71,6 @@ fun OverviewStatusSection( commandsAllowed: Boolean = true, onNavigate: (NavigationRequest) -> Unit, statusLightsDef: PreferenceSubScreenDef, - onCopyFromNightscout: () -> Unit, expanded: Boolean, onExpandedChange: (Boolean) -> Unit, modifier: Modifier = Modifier @@ -183,7 +180,6 @@ fun OverviewStatusSection( StatusLightsSettingsBottomSheet( settingsDef = statusLightsDef, onDismiss = { showSettingsSheet = false }, - onCopyFromNightscout = onCopyFromNightscout, sheetState = sheetState ) } @@ -194,7 +190,6 @@ fun OverviewStatusSection( private fun StatusLightsSettingsBottomSheet( settingsDef: PreferenceSubScreenDef, onDismiss: () -> Unit, - onCopyFromNightscout: () -> Unit, sheetState: SheetState ) { ModalBottomSheet( @@ -202,20 +197,12 @@ private fun StatusLightsSettingsBottomSheet( sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surface ) { - StatusLightsSettingsContent( - settingsDef = settingsDef, - onCopyFromNightscout = onCopyFromNightscout - ) + StatusLightsSettingsContent(settingsDef = settingsDef) } } @Composable -internal fun StatusLightsSettingsContent( - settingsDef: PreferenceSubScreenDef, - onCopyFromNightscout: () -> Unit -) { - var showCopyDialog by remember { mutableStateOf(false) } - +internal fun StatusLightsSettingsContent(settingsDef: PreferenceSubScreenDef) { Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -231,27 +218,6 @@ internal fun StatusLightsSettingsContent( // Multiple group subscreens → one expandable card each (collapsed by default), matching the // main Settings screen's look. Shared renderer; groups are the SSOT in BuiltInSearchables. PreferenceSheetContent(settingsDef = settingsDef) - - FilledTonalButton( - onClick = { showCopyDialog = true }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Text(text = stringResource(app.aaps.core.ui.R.string.copy_existing_values)) - } - } - - if (showCopyDialog) { - OkCancelDialog( - title = stringResource(app.aaps.core.ui.R.string.statuslights), - message = stringResource(app.aaps.core.ui.R.string.copy_existing_values), - onConfirm = { - onCopyFromNightscout() - showCopyDialog = false - }, - onDismiss = { showCopyDialog = false } - ) } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSectionPreviews.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSectionPreviews.kt index f782d0ade368..864fd15fdce3 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSectionPreviews.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewStatusSectionPreviews.kt @@ -63,7 +63,6 @@ internal fun OverviewStatusSectionCollapsedPreview() { showPumpBatteryChange = true, onNavigate = {}, statusLightsDef = previewStatusLightsDef, - onCopyFromNightscout = {}, expanded = false, onExpandedChange = {} ) @@ -84,7 +83,6 @@ internal fun OverviewStatusSectionExpandedPreview() { showPumpBatteryChange = true, onNavigate = {}, statusLightsDef = previewStatusLightsDef, - onCopyFromNightscout = {}, expanded = true, onExpandedChange = {} ) diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt index 4d4d5f1b8457..fd4fe5d7e359 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt @@ -8,8 +8,6 @@ import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.db.ProcessedTbrEbData import app.aaps.core.interfaces.logging.AAPSLogger -import app.aaps.core.interfaces.logging.UserEntryLogger -import app.aaps.core.interfaces.nsclient.NSSettingsStatus import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.plugin.PluginDescription @@ -59,10 +57,8 @@ internal class ManageViewModelTest { @Mock private lateinit var config: Config @Mock private lateinit var processedTbrEbData: ProcessedTbrEbData @Mock private lateinit var persistenceLayer: PersistenceLayer - @Mock private lateinit var uel: UserEntryLogger @Mock private lateinit var rxBus: RxBus @Mock private lateinit var dateUtil: DateUtil - @Mock private lateinit var nsSettingStatus: NSSettingsStatus @Mock private lateinit var preferences: Preferences @Mock private lateinit var batchExecutor: BatchExecutor @Mock private lateinit var nsClient: NsClient @@ -87,8 +83,8 @@ internal class ManageViewModelTest { pumpPlugin = mock() whenever(activePlugin.activePumpInternal).thenReturn(pumpPlugin) sut = ManageViewModel( - rh, activePlugin, profileFunction, loop, config, processedTbrEbData, persistenceLayer, uel, - rxBus, dateUtil, nsSettingStatus, preferences, batchExecutor, nsClient, visibilityContext, + rh, activePlugin, profileFunction, loop, config, processedTbrEbData, persistenceLayer, + rxBus, dateUtil, preferences, batchExecutor, nsClient, visibilityContext, CoroutineScope(UnconfinedTestDispatcher()) ) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/overview/StatusLightsSettingsContentTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/overview/StatusLightsSettingsContentTest.kt index 9cf417ae3341..3de9bda3ae1b 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/overview/StatusLightsSettingsContentTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/overview/StatusLightsSettingsContentTest.kt @@ -48,8 +48,7 @@ class StatusLightsSettingsContentTest { CompositionLocalProvider(LocalPreferences provides preferences, LocalConfig provides config) { MaterialTheme { StatusLightsSettingsContent( - settingsDef = PreferenceSubScreenDef(key = "statuslights", titleResId = CoreUiR.string.treatments), - onCopyFromNightscout = {} + settingsDef = PreferenceSubScreenDef(key = "statuslights", titleResId = CoreUiR.string.treatments) ) } } From aff51ea09910869b2bead5073e289bb641c9b61b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 18:02:47 +0200 Subject: [PATCH 074/146] Last org.json out of core:interfaces: exportApsResult and APSResult.json exportApsResult only ever writes its arguments to a debug file, so it takes the documents as text now and the worker nests them with kotlinx. Passing already serialised JSON to org.json's put() would have quoted and escaped the whole document into a single string value, silently, so there is a test holding that shut. APSResult.json() returns a kotlinx JsonObject. DetermineBasalResult built it by serialising RT to text and parsing that back with org.json, once per loop cycle; it encodes straight to a tree now. The same default Json still rejects NaN and Infinity, so the deliberate crash signal is unchanged. LoopPlugin used to write into the document returned by json(). That document cannot be written into any more, so it merges through a new JsonObject.with(builder) helper. The androidTest replay suite reads its documents back through org.json, which keeps all 61 assertions there exactly as they were written. One visible change: whole numbers print as "1.0" where they printed as "1" in the DeviceStatus payload. org.json trims a trailing zero when it prints and kotlinx does not. Same JSON number, and it matches what RT.serialize has always produced. Tests: buildAndStoreDeviceStatus had no coverage at all and has five cases now, JsonObject.with has six, and the content of json() has three. --- .../kotlin/app/aaps/ReplayApsResultsTest.kt | 133 ++++++++++-------- .../plugins/aps/openAPS/APSResultObject.kt | 14 +- .../DetermineBasalResultAMAFromJS.kt | 7 +- .../aps/openAPSAMA/TestOpenAPSAMAPlugin.kt | 11 +- .../DetermineBasalResultSMBFromJS.kt | 7 +- .../aps/openAPSSMB/TestOpenAPSSMBPlugin.kt | 7 +- .../app/aaps/core/interfaces/aps/APSResult.kt | 4 +- .../maintenance/ImportExportPrefs.kt | 9 +- .../core/objects/extensions/JSONObjectExt.kt | 15 ++ .../objects/extensions/JSONObjectExtTest.kt | 93 +++++++++++- .../aps/DetermineBasalResult.kt | 15 +- .../maintenance/ImportExportPrefsImpl.kt | 26 +++- .../aps/DetermineBasalResultTest.kt | 45 ++++++ .../maintenance/ApsResultExportWorkerTest.kt | 55 +++++++- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 42 +++--- .../aaps/plugins/aps/loop/LoopPluginTest.kt | 127 +++++++++++++++++ 16 files changed, 501 insertions(+), 109 deletions(-) diff --git a/app/src/androidTest/kotlin/app/aaps/ReplayApsResultsTest.kt b/app/src/androidTest/kotlin/app/aaps/ReplayApsResultsTest.kt index 35b132dac3c0..9b2b1fdbacac 100644 --- a/app/src/androidTest/kotlin/app/aaps/ReplayApsResultsTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/ReplayApsResultsTest.kt @@ -3,6 +3,7 @@ package app.aaps import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.interfaces.aps.APSResult import app.aaps.core.interfaces.aps.AutosensResult import app.aaps.core.interfaces.aps.CurrentTemp import app.aaps.core.interfaces.aps.GlucoseStatusAutoIsf @@ -118,7 +119,7 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { JSONAssert.assertEquals( "Error in file $filename", output.toString(), - result?.json()?.apply { + result?.jsonOrg()?.apply { // this is added afterwards to json. Copy from original put("timestamp", output.getString("timestamp")) }.toString(), @@ -239,22 +240,22 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { aapsLogger.info(LTag.APS, resultKt.toString()) - aapsLogger.debug(LTag.APS, result?.json()?.getString("reason") ?: "") + aapsLogger.debug(LTag.APS, result?.jsonOrg()?.getString("reason") ?: "") aapsLogger.debug(LTag.APS, resultKt.reason.toString()) aapsLogger.debug(LTag.APS, "File: $filename") // assertThat(resultKt.reason.toString()).isEqualTo(result?.json?.getString("reason")) - assertThat(resultKt.tick ?: "").isEqualTo(result?.json()?.optString("tick")) - assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("eventualBG")) - assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("targetBG")) - assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.json()?.optDouble("insulinReq")) - assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.json()?.optInt("carbsReq")) - assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.json()?.optInt("carbsReqWithin")) - assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.json()?.optDouble("units")) - assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.json()?.optDouble("sensitivityRatio")) - assertThat(resultKt.duration ?: 0).isEqualTo(result?.json()?.optInt("duration")) - assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.json()?.optDouble("rate")) - assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("COB")) - assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("IOB")) + assertThat(resultKt.tick ?: "").isEqualTo(result?.jsonOrg()?.optString("tick")) + assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("eventualBG")) + assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("targetBG")) + assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("insulinReq")) + assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReq")) + assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReqWithin")) + assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("units")) + assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("sensitivityRatio")) + assertThat(resultKt.duration ?: 0).isEqualTo(result?.jsonOrg()?.optInt("duration")) + assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("rate")) + assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("COB")) + assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("IOB")) } private fun testOpenAPSSMBDynamicISF(filename: String, input: JSONObject, output: JSONObject, injector: HasAndroidInjector) { @@ -283,7 +284,7 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { JSONAssert.assertEquals( "Error in file $filename", output.toString(), - result?.json()?.apply { + result?.jsonOrg()?.apply { // this is added afterwards to json. Copy from original put("timestamp", output.getString("timestamp")) }.toString(), @@ -404,23 +405,23 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { aapsLogger.info(LTag.APS, resultKt.toString()) - aapsLogger.debug(LTag.APS, result?.json()?.getString("reason") ?: "") + aapsLogger.debug(LTag.APS, result?.jsonOrg()?.getString("reason") ?: "") aapsLogger.debug(LTag.APS, resultKt.reason.toString()) aapsLogger.debug(LTag.APS, "File: $filename") -// assertThat(resultKt.reason.toString()).isEqualTo(result?.json()?.getString("reason")) - assertThat(resultKt.tick ?: "").isEqualTo(result?.json()?.optString("tick")) - assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("eventualBG")) - assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("targetBG")) - assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.json()?.optDouble("insulinReq")) - assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.json()?.optInt("carbsReq")) - assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.json()?.optInt("carbsReqWithin")) - assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.json()?.optDouble("units")) - assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.json()?.optDouble("sensitivityRatio")) - assertThat(resultKt.duration ?: 0).isEqualTo(result?.json()?.optInt("duration")) - assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.json()?.optDouble("rate")) - assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("COB")) - assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("IOB")) - assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result?.json()?.optDouble("variable_sens")) +// assertThat(resultKt.reason.toString()).isEqualTo(result?.jsonOrg()?.getString("reason")) + assertThat(resultKt.tick ?: "").isEqualTo(result?.jsonOrg()?.optString("tick")) + assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("eventualBG")) + assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("targetBG")) + assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("insulinReq")) + assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReq")) + assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReqWithin")) + assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("units")) + assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("sensitivityRatio")) + assertThat(resultKt.duration ?: 0).isEqualTo(result?.jsonOrg()?.optInt("duration")) + assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("rate")) + assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("COB")) + assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("IOB")) + assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("variable_sens")) } private fun testOpenAPSAMA(filename: String, input: JSONObject, output: JSONObject, injector: HasAndroidInjector) { @@ -442,7 +443,7 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { JSONAssert.assertEquals( "Error in file $filename", output.toString(), - result?.json()?.apply { + result?.jsonOrg()?.apply { // this is added afterwards to json. Copy from original put("timestamp", output.getString("timestamp")) }.toString(), @@ -558,23 +559,23 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { aapsLogger.info(LTag.APS, resultKt.toString()) - aapsLogger.debug(LTag.APS, result?.json()?.getString("reason") ?: "") + aapsLogger.debug(LTag.APS, result?.jsonOrg()?.getString("reason") ?: "") aapsLogger.debug(LTag.APS, resultKt.reason.toString()) aapsLogger.debug(LTag.APS, "File: $filename") -// assertThat(resultKt.reason.toString()).isEqualTo(result?.json()?.getString("reason")) - assertThat(resultKt.tick ?: "").isEqualTo(result?.json()?.optString("tick")) - assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("eventualBG")) - assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.json()?.optDouble("targetBG")) - assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.json()?.optDouble("insulinReq")) - assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.json()?.optInt("carbsReq")) - assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.json()?.optInt("carbsReqWithin")) - assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.json()?.optDouble("units")) - assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.json()?.optDouble("sensitivityRatio")) - assertThat(resultKt.duration ?: 0).isEqualTo(result?.json()?.optInt("duration")) - assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.json()?.optDouble("rate")) - assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("COB")) - assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.json()?.optDouble("IOB")) - assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result?.json()?.optDouble("variable_sens")) +// assertThat(resultKt.reason.toString()).isEqualTo(result?.jsonOrg()?.getString("reason")) + assertThat(resultKt.tick ?: "").isEqualTo(result?.jsonOrg()?.optString("tick")) + assertThat(resultKt.eventualBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("eventualBG")) + assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("targetBG")) + assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("insulinReq")) + assertThat(resultKt.carbsReq ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReq")) + assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result?.jsonOrg()?.optInt("carbsReqWithin")) + assertThat(resultKt.units ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("units")) + assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("sensitivityRatio")) + assertThat(resultKt.duration ?: 0).isEqualTo(result?.jsonOrg()?.optInt("duration")) + assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("rate")) + assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("COB")) + assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("IOB")) + assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result?.jsonOrg()?.optDouble("variable_sens")) } private fun testOpenAPSSMBAutoISF(filename: String, input: JSONObject, output: JSONObject, injector: HasAndroidInjector) { @@ -599,7 +600,7 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { JSONAssert.assertEquals( "Error in file $filename", output.toString(), - result.json()?.apply { + result.jsonOrg()?.apply { // this is added afterwards to json. Copy from original put("timestamp", output.getString("timestamp")) }.toString(), @@ -750,23 +751,23 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { // // aapsLogger.info(LTag.APS, resultKt.toString()) // - // aapsLogger.debug(LTag.APS, result?.json()?.getString("reason") ?: "") + // aapsLogger.debug(LTag.APS, result?.jsonOrg()?.getString("reason") ?: "") // aapsLogger.debug(LTag.APS, resultKt.reason.toString()) aapsLogger.debug(LTag.APS, "File: $filename") // // assertThat(resultKt.reason.toString()).isEqualTo(result?.json?.getString("reason")) - assertThat(resultKt.tick ?: "").isEqualTo(result.json()?.optString("tick")) - assertThat(resultKt.eventualBG ?: 0.0).isWithin(1.0).of(result.json()?.optDouble("eventualBG") ?: 0.0) - assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result.json()?.optDouble("targetBG")) - assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result.json()?.optDouble("insulinReq")) - assertThat(resultKt.carbsReq ?: 0).isEqualTo(result.json()?.optInt("carbsReq")) - assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result.json()?.optInt("carbsReqWithin")) - assertThat(resultKt.units ?: Double.NaN).isEqualTo(result.json()?.optDouble("units")) - assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result.json()?.optDouble("sensitivityRatio")) - assertThat(resultKt.duration ?: 0).isEqualTo(result.json()?.optInt("duration")) - assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result.json()?.optDouble("rate")) - assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result.json()?.optDouble("COB")) - assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result.json()?.optDouble("IOB")) - assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result.json()?.optDouble("variable_sens")) + assertThat(resultKt.tick ?: "").isEqualTo(result.jsonOrg()?.optString("tick")) + assertThat(resultKt.eventualBG ?: 0.0).isWithin(1.0).of(result.jsonOrg()?.optDouble("eventualBG") ?: 0.0) + assertThat(resultKt.targetBG ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("targetBG")) + assertThat(resultKt.insulinReq ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("insulinReq")) + assertThat(resultKt.carbsReq ?: 0).isEqualTo(result.jsonOrg()?.optInt("carbsReq")) + assertThat(resultKt.carbsReqWithin ?: 0).isEqualTo(result.jsonOrg()?.optInt("carbsReqWithin")) + assertThat(resultKt.units ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("units")) + assertThat(resultKt.sensitivityRatio ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("sensitivityRatio")) + assertThat(resultKt.duration ?: 0).isEqualTo(result.jsonOrg()?.optInt("duration")) + assertThat(resultKt.rate ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("rate")) + assertThat(resultKt.COB ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("COB")) + assertThat(resultKt.IOB ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("IOB")) + assertThat(resultKt.variable_sens ?: Double.NaN).isEqualTo(result.jsonOrg()?.optDouble("variable_sens")) } enum class TestSource { ASSET, FILE } @@ -804,4 +805,14 @@ class ReplayApsResultsTest : HiltInstrumentedTest() { TestSource.FILE -> JSONObject(storage.getFileContents(File(path))).apply { put("filename", name) } } } + + /** + * [APSResult.json] hands back an immutable kotlinx document now. Every assertion in this file was + * written against `org.json` and leans on its accessor defaults - `optDouble` gives NaN for a + * missing key, `optInt` gives 0, `optString` gives "" - and two places still add a timestamp to + * the document before comparing it. Reading the same bytes back through `org.json` keeps all of + * that exactly as written, which also makes this file say that the new document is the same + * document. This is androidTest, so the extra parse costs nothing. + */ + private fun APSResult.jsonOrg(): JSONObject? = json()?.let { JSONObject(it.toString()) } } diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt index 385115077116..93e21b6ce240 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPS/APSResultObject.kt @@ -31,7 +31,9 @@ import app.aaps.core.objects.extensions.convertedToPercent import app.aaps.core.ui.R import dagger.android.HasAndroidInjector import kotlinx.coroutines.runBlocking -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import javax.inject.Inject import kotlin.math.abs import kotlin.math.max @@ -147,14 +149,12 @@ open class APSResultObject(protected val injector: HasAndroidInjector) : APSResu newResult.targetBG = targetBG } - override fun json(): JSONObject? { - val json = JSONObject() + override fun json(): JsonObject? = buildJsonObject { if (runBlocking { isChangeRequested() }) { - json.put("rate", rate) - json.put("duration", duration) - json.put("reason", reason) + put("rate", rate) + put("duration", duration) + put("reason", reason) } - return json } override val predictionsAsGv: MutableList diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalResultAMAFromJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalResultAMAFromJS.kt index 06417a00ad81..e2673d07adc5 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalResultAMAFromJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalResultAMAFromJS.kt @@ -4,6 +4,9 @@ import app.aaps.core.interfaces.aps.Predictions import app.aaps.core.interfaces.utils.DateUtil import app.aaps.plugins.aps.openAPS.APSResultObject import dagger.android.HasAndroidInjector +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import org.json.JSONObject import org.mozilla.javascript.NativeObject import javax.inject.Inject @@ -56,7 +59,9 @@ class DetermineBasalResultAMAFromJS @Inject constructor(injector: HasAndroidInje return newResult } - override fun json(): JSONObject? = json + // The JS bridge hands over an org.json document and predictions() reads predBGs out of it, so the + // field stays as it is and only the contract is converted. androidTest, so a reparse costs nothing. + override fun json(): JsonObject? = json?.let { Json.parseToJsonElement(it.toString()).jsonObject } override fun predictions(): Predictions { val predictions = Predictions() diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt index d965cfa7de7f..f2c174e2f59d 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt @@ -36,6 +36,7 @@ import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.target +import app.aaps.core.objects.extensions.with import app.aaps.core.utils.MidnightUtils import app.aaps.plugins.aps.R import app.aaps.plugins.aps.events.EventOpenAPSUpdateGui @@ -43,6 +44,7 @@ import app.aaps.plugins.aps.events.EventResetOpenAPSGui import app.aaps.plugins.aps.openAPSSMB.GlucoseStatusCalculatorSMB import app.aaps.plugins.aps.utils.ScriptReader import dagger.android.HasAndroidInjector +import kotlinx.serialization.json.put import org.json.JSONException import javax.inject.Inject import javax.inject.Singleton @@ -229,13 +231,18 @@ class TestOpenAPSAMAPlugin @Inject constructor( false //determineBasalResultAMA.iob = iobArray[0] val now = System.currentTimeMillis() - determineBasalResultAMA.json()?.put("timestamp", dateUtil.toISOString(now)) determineBasalResultAMA.inputConstraints = inputConstraints //lastDetermineBasalAdapter = determineBasalAdapterAMAJS lastAPSResult = determineBasalResultAMA as DetermineBasalResultAMAFromJS lastAPSRun = now if (config.isEnabled(ExternalOptions.UNFINISHED_MODE)) - importExportPrefs.exportApsResult(this::class.simpleName, determineBasalAdapterAMAJS.json(), determineBasalResultAMA.json()) + importExportPrefs.exportApsResult( + this::class.simpleName, + determineBasalAdapterAMAJS.json().toString(), + // The timestamp used to be put into the stored document a few lines up. That document + // is immutable now, so it goes on here, where it is actually used. + determineBasalResultAMA.json()?.with { put("timestamp", dateUtil.toISOString(now)) }?.toString() + ) rxBus.send(EventAPSCalculationFinished()) } rxBus.send(EventOpenAPSUpdateGui()) diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalResultSMBFromJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalResultSMBFromJS.kt index 7aa080477b69..0dc95b9a9787 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalResultSMBFromJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalResultSMBFromJS.kt @@ -6,6 +6,9 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.plugins.aps.openAPS.APSResultObject import dagger.android.HasAndroidInjector import org.json.JSONException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import org.json.JSONObject import javax.inject.Inject @@ -73,7 +76,9 @@ class DetermineBasalResultSMBFromJS private constructor(injector: HasAndroidInje return newResult } - override fun json(): JSONObject? = json + // The JS bridge hands over an org.json document and predictions() reads predBGs out of it, so the + // field stays as it is and only the contract is converted. androidTest, so a reparse costs nothing. + override fun json(): JsonObject? = json?.let { Json.parseToJsonElement(it.toString()).jsonObject } override fun predictions(): Predictions { val predictions = Predictions() diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt index 306ed85b79e0..14b4ff43c824 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt @@ -282,7 +282,9 @@ open class TestOpenAPSSMBPlugin @Inject constructor( .isTempBasalRequested = false //determineBasalResultSMB.iob = iobArray[0] - determineBasalResultSMB.json()?.put("timestamp", dateUtil.toISOString(now)) + // The timestamp put() that stood here wrote into the stored document for the export + // below, which is commented out. The document is immutable now, so it is added at the + // export instead - see the commented call and TestOpenAPSAMAPlugin for the live one. determineBasalResultSMB.inputConstraints = inputConstraints //lastDetermineBasalAdapter = determineBasalAdapterSMBJS lastAPSResult = determineBasalResultSMB as DetermineBasalResultSMBFromJS @@ -293,7 +295,8 @@ open class TestOpenAPSSMBPlugin @Inject constructor( // is DetermineBasalAdapterSMBJS -> OpenAPSSMBPlugin::class.simpleName // is DetermineBasalAdapterSMBDynamicISFJS -> OpenAPSSMBDynamicISFPlugin::class.simpleName // else -> "Error" - // }, determineBasalAdapterSMBJS.json(), determineBasalResultSMB.json() + // }, determineBasalAdapterSMBJS.json().toString(), + // determineBasalResultSMB.json()?.with { put("timestamp", dateUtil.toISOString(now)) }?.toString() // ) rxBus.send(EventAPSCalculationFinished()) } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt index ec35b660e1da..eeb8981bd793 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt @@ -2,7 +2,7 @@ package app.aaps.core.interfaces.aps import app.aaps.core.data.model.GV import app.aaps.core.interfaces.constraints.Constraint -import org.json.JSONObject +import kotlinx.serialization.json.JsonObject interface APSResult { @@ -52,7 +52,7 @@ interface APSResult { suspend fun resultAsString(): String fun newAndClone(): APSResult - fun json(): JSONObject? + fun json(): JsonObject? fun predictions(): Predictions? fun rawData(): Any diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt index 84877d44fa27..05006397d7b6 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt @@ -3,7 +3,6 @@ package app.aaps.core.interfaces.maintenance import android.content.Context import androidx.fragment.app.FragmentActivity import app.aaps.core.interfaces.rx.weardata.CwfData -import org.json.JSONObject /** Where to send the export. */ enum class ExportDestination { @@ -54,7 +53,13 @@ interface ImportExportPrefs { fun exportSharedPreferencesNonInteractive(password: String): Boolean fun exportUserEntriesCsv() suspend fun executeCsvExport(): ExportResult - fun exportApsResult(algorithm: String?, input: JSONObject, output: JSONObject?) + /** + * Write one APS run to a debug file (engineering mode only). + * + * [input] and [output] are whole JSON documents, already serialised. Nothing here reads them - + * they are wrapped in a small envelope and written out - so text is all this needs. + */ + fun exportApsResult(algorithm: String?, input: String, output: String?) // Compose export support — discrete steps, no UI diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt index 437290eb27a4..528ddf42191e 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/JSONObjectExt.kt @@ -7,7 +7,9 @@ import app.aaps.core.keys.interfaces.LongPreferenceKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringNonPreferenceKey import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.json.JSONObject @@ -92,3 +94,16 @@ fun JsonObjectBuilder.putIfThereIsValue(key: String, value: Double?) { fun JsonObjectBuilder.putIfThereIsValue(key: String, value: String?) { if (value != null && value.isNotEmpty()) put(key, value) } + +/** + * Copy of this document with [extra] entries added on top. + * + * A [JsonObject] cannot be written into after it is built, so code that used to take a finished + * document and keep putting into it needs a new document instead. Entries added here win over + * entries of the same name already present, which is what writing into the old object did. + */ +fun JsonObject.with(extra: JsonObjectBuilder.() -> Unit): JsonObject = + buildJsonObject { + this@with.forEach { (key, value) -> put(key, value) } + extra() + } diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/JSONObjectExtTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/JSONObjectExtTest.kt index fca0f4cf05af..00aa99dc15aa 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/JSONObjectExtTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/extensions/JSONObjectExtTest.kt @@ -7,13 +7,24 @@ import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import org.json.JSONObject import org.junit.jupiter.api.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -/** Covers the org.json [JSONObject] preference extensions: putIfThereIsValue skip rules + put/store round-trip. */ +/** + * Covers the [JSONObject] preference extensions (putIfThereIsValue skip rules + put/store round-trip) + * and their kotlinx twins, including [with], which replaced writing into an already built document. + */ class JSONObjectExtTest { @Test @@ -90,4 +101,84 @@ class JSONObjectExtTest { JSONObject().store(IntKey.OverviewCarbsButtonIncrement1, prefs) // key not present → no-op // nothing to verify beyond no exception; getInt on absent key would have thrown inside store } + + // region kotlinx twins + + @Test + fun kotlinxPutIfThereIsValue_writesNonZeroSkipsZeroAndNull() { + val j = buildJsonObject { + putIfThereIsValue("i", 5) + putIfThereIsValue("iz", 0) + putIfThereIsValue("inull", null as Int?) + putIfThereIsValue("l", 5L) + putIfThereIsValue("lz", 0L) + putIfThereIsValue("d", 1.5) + putIfThereIsValue("dz", 0.0) + putIfThereIsValue("s", "x") + putIfThereIsValue("se", "") + } + assertThat(j.getValue("i").jsonPrimitive.int).isEqualTo(5) + assertThat(j.containsKey("iz")).isFalse() + assertThat(j.containsKey("inull")).isFalse() + assertThat(j.getValue("l").jsonPrimitive.long).isEqualTo(5L) + assertThat(j.containsKey("lz")).isFalse() + assertThat(j.getValue("d").jsonPrimitive.double).isEqualTo(1.5) + assertThat(j.containsKey("dz")).isFalse() + assertThat(j.getValue("s").jsonPrimitive.content).isEqualTo("x") + assertThat(j.containsKey("se")).isFalse() + } + + /** + * [with] replaces code that used to keep writing into a finished document, so it has to behave the + * same way a repeated `put` did: entries add, and a repeat of an existing name replaces it. + */ + @Test + fun with_addsEntries() { + val base = buildJsonObject { put("a", 1) } + + val merged = base.with { put("b", 2) } + + assertThat(merged.getValue("a").jsonPrimitive.int).isEqualTo(1) + assertThat(merged.getValue("b").jsonPrimitive.int).isEqualTo(2) + } + + @Test + fun with_laterEntryWinsOverExistingOne() { + val base = buildJsonObject { put("a", 1) } + + val merged = base.with { put("a", 99) } + + assertThat(merged.getValue("a").jsonPrimitive.int).isEqualTo(99) + assertThat(merged).hasSize(1) + } + + /** The source is a value: merging must hand back a new document and leave the old one alone. */ + @Test + fun with_leavesTheSourceUntouched() { + val base = buildJsonObject { put("a", 1) } + + base.with { put("b", 2) } + + assertThat(base.containsKey("b")).isFalse() + assertThat(base).hasSize(1) + } + + @Test + fun with_addingNothingGivesAnEqualDocument() { + val base = buildJsonObject { put("a", 1); put("b", "x") } + + assertThat(base.with { }).isEqualTo(base) + } + + /** Nested values survive the copy - the merge is shallow, so the sub-document is carried over as is. */ + @Test + fun with_keepsNestedDocuments() { + val base = buildJsonObject { putJsonObject("inner") { put("deep", 7) } } + + val merged = base.with { put("a", 1) } + + assertThat(merged.getValue("inner").jsonObject.getValue("deep").jsonPrimitive.int).isEqualTo(7) + } + + // endregion } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt b/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt index 54c9639e4e2b..73088cf94002 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/aps/DetermineBasalResult.kt @@ -31,7 +31,9 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertedToAbsolute import app.aaps.core.objects.extensions.convertedToPercent import app.aaps.core.ui.R -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import javax.inject.Inject import javax.inject.Provider import kotlin.math.abs @@ -141,9 +143,12 @@ class DetermineBasalResult @Inject constructor( } override fun newAndClone(): APSResult = apsResultProvider.get().with(result) - override fun json(): JSONObject { + override fun json(): JsonObject { reportNonFiniteResultFields() - return JSONObject(result.serialize()) + // Straight to a tree. This used to serialise to text and parse it back with org.json, once per + // loop cycle. Same default Json either way, so a non-finite Double still throws here - see + // `reportNonFiniteResultFields`, that crash is the signal and must not be swallowed. + return Json.encodeToJsonElement(RT.serializer(), result).jsonObject } /** @@ -155,9 +160,9 @@ class DetermineBasalResult @Inject constructor( * * We deliberately do NOT sanitize/swallow here — the crash is the signal driving the ongoing * DetermineBasal NaN hunt (see `DetermineBasalSMB` minPredBG pin, `OpenAPSSMBPlugin` invalidInputs - * guard). Instead, right before the (still-crashing) serialize, we report exactly which field is + * guard). Instead, right before the (still-crashing) encode, we report exactly which field is * non-finite plus the ISF inputs that feed it, so the next occurrence pinpoints the field and - * algorithm instead of an opaque framework trace. `[result.serialize]` runs unchanged afterwards. + * algorithm instead of an opaque framework trace. The encode runs unchanged afterwards. */ private fun reportNonFiniteResultFields() { val offenders = buildList { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt index 5551f1398246..f259b3fa2d6c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/maintenance/ImportExportPrefsImpl.kt @@ -75,7 +75,10 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.json.JSONObject +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import java.io.FileNotFoundException import java.io.IOException import java.time.LocalDateTime @@ -1135,7 +1138,7 @@ class ImportExportPrefsImpl @Inject constructor( } } - override fun exportApsResult(algorithm: String?, input: JSONObject, output: JSONObject?) { + override fun exportApsResult(algorithm: String?, input: String, output: String?) { dataInbox.putAndEnqueue(ApsExportInbox, ApsResultExportWorker.ApsResultData(algorithm, input, output)) } @@ -1151,7 +1154,8 @@ class ImportExportPrefsImpl @Inject constructor( private val dataInbox: DataInbox ) : LoggingWorker(context, params, Dispatchers.IO, aapsLogger, fabricPrivacy) { - data class ApsResultData(val algorithm: String?, val input: JSONObject, val output: JSONObject?) + /** [input] and [output] are already serialised JSON documents. */ + data class ApsResultData(val algorithm: String?, val input: String, val output: String?) override suspend fun doWorkAndLog(): Result { if (!config.isEngineeringMode()) return Result.success(workDataOf("Result" to "Export not enabled")) @@ -1163,10 +1167,13 @@ class ImportExportPrefsImpl @Inject constructor( for (apsResultData in items) { val newFile = prefFileList.newResultFile() try { - val jsonObject = JSONObject().apply { - put("algorithm", apsResultData.algorithm) - put("input", apsResultData.input) - put("output", apsResultData.output) + // parseToJsonElement, not put(key, text): the documents must be nested, and writing + // them as text would quote and escape the whole thing into a single string value. + val jsonObject = buildJsonObject { + // A null algorithm left the key out before. Keep it out. + apsResultData.algorithm?.let { put("algorithm", it) } + put("input", Json.parseToJsonElement(apsResultData.input)) + apsResultData.output?.let { put("output", Json.parseToJsonElement(it)) } } storage.putFileContents(newFile, jsonObject.toString()) } catch (e: FileNotFoundException) { @@ -1175,6 +1182,11 @@ class ImportExportPrefsImpl @Inject constructor( } catch (e: IOException) { aapsLogger.error(LTag.CORE, "Unhandled exception", e) hadFailure = true + } catch (e: SerializationException) { + // Only reachable now that the documents arrive as text - a caller that hands us + // something unparsable loses this one file instead of taking the worker down. + aapsLogger.error(LTag.CORE, "APS result is not valid JSON", e) + hadFailure = true } } return if (hadFailure) Result.failure(workDataOf("Error" to "one or more exports failed")) else Result.success() diff --git a/implementation/src/test/kotlin/app/aaps/implementation/aps/DetermineBasalResultTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/aps/DetermineBasalResultTest.kt index 1ee95e512324..efe9fb09bef4 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/aps/DetermineBasalResultTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/aps/DetermineBasalResultTest.kt @@ -4,6 +4,9 @@ import app.aaps.core.interfaces.aps.APSResult import app.aaps.core.interfaces.aps.RT import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.any @@ -57,4 +60,46 @@ class DetermineBasalResultTest : TestBaseWithProfile() { verify(fabricPrivacy, never()).logException(any()) } + + /** + * What the document actually contains. The other tests only say whether `json()` throws, which left + * the payload uploaded to Nightscout every loop cycle unasserted. + */ + @Test + fun `json carries the result fields`() { + val result = apsResultProvider.get() + .with(RT(runningDynamicIsf = false, algorithm = APSResult.Algorithm.SMB, eventualBG = 120.0, insulinReq = 0.5, rate = 1.5, duration = 30)) + + val json = result.json()!! + + assertThat(json.getValue("eventualBG").jsonPrimitive.double).isEqualTo(120.0) + assertThat(json.getValue("insulinReq").jsonPrimitive.double).isEqualTo(0.5) + assertThat(json.getValue("rate").jsonPrimitive.double).isEqualTo(1.5) + assertThat(json.getValue("duration").jsonPrimitive.int).isEqualTo(30) + } + + /** A field the result did not set stays out of the document rather than going out as null. */ + @Test + fun `json omits fields that were not set`() { + val result = apsResultProvider.get() + .with(RT(runningDynamicIsf = false, algorithm = APSResult.Algorithm.SMB, eventualBG = 120.0)) + + assertThat(result.json()!!.containsKey("insulinReq")).isFalse() + } + + /** + * Whole numbers print as `1.0`, not `1`. + * + * This changed when `json()` stopped going through org.json on the way out: org.json trims a + * trailing `.0` when it prints, kotlinx does not. Both are the same JSON number and Nightscout reads + * them the same way, but the bytes stored in DeviceStatus differ, so it is stated here rather than + * left to be noticed on a server. It also matches what `RT.serialize` has always produced. + */ + @Test + fun `a whole number keeps its decimal point`() { + val result = apsResultProvider.get() + .with(RT(runningDynamicIsf = false, algorithm = APSResult.Algorithm.SMB, rate = 1.0, duration = 30)) + + assertThat(result.json().toString()).contains("\"rate\":1.0") + } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/maintenance/ApsResultExportWorkerTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/maintenance/ApsResultExportWorkerTest.kt index 68088371aacc..291e9a2f7963 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/maintenance/ApsResultExportWorkerTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/maintenance/ApsResultExportWorkerTest.kt @@ -6,6 +6,7 @@ import app.aaps.core.interfaces.maintenance.FileListProvider import app.aaps.core.interfaces.storage.Storage import app.aaps.core.utils.receivers.DataInbox import app.aaps.shared.tests.TestBaseWithProfile +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.json.JSONObject import org.junit.jupiter.api.Assertions @@ -13,6 +14,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doThrow import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -31,8 +33,8 @@ class ApsResultExportWorkerTest : TestBaseWithProfile() { private fun worker() = ImportExportPrefsImpl.ApsResultExportWorker(context, workerParameters, aapsLogger, fabricPrivacy, prefFileList, storage, config, dataInbox) - private fun apsData() = - ImportExportPrefsImpl.ApsResultExportWorker.ApsResultData("SMB", JSONObject().put("a", 1), JSONObject().put("b", 2)) + private fun apsData(input: String = """{"a":1}""", output: String? = """{"b":2}""") = + ImportExportPrefsImpl.ApsResultExportWorker.ApsResultData("SMB", input, output) @BeforeEach fun setup() { @@ -82,4 +84,53 @@ class ApsResultExportWorkerTest : TestBaseWithProfile() { assertIs(result) } + + /** + * The documents arrive as text and must end up **nested** in the envelope. Writing them with a + * plain string put would quote and escape each one into a single string value - which compiles, + * never throws, and silently produces a file the tooling can no longer read. `getJSONObject` + * fails on a string value, so this test bites exactly there. + */ + @Test + fun `documents are nested rather than written as escaped text`() = runTest { + whenever(config.isEngineeringMode()).thenReturn(true) + whenever(dataInbox.drain(ApsExportInbox)).thenReturn(listOf(apsData())) + val written = argumentCaptor() + + worker().doWorkAndLog() + + verify(storage).putFileContents(any(), written.capture()) + val json = JSONObject(written.firstValue) + assertThat(json.getString("algorithm")).isEqualTo("SMB") + assertThat(json.getJSONObject("input").getInt("a")).isEqualTo(1) + assertThat(json.getJSONObject("output").getInt("b")).isEqualTo(2) + } + + /** A run with no output left the key out before, and still does. */ + @Test + fun `a missing output leaves the key out`() = runTest { + whenever(config.isEngineeringMode()).thenReturn(true) + whenever(dataInbox.drain(ApsExportInbox)).thenReturn(listOf(apsData(output = null))) + val written = argumentCaptor() + + worker().doWorkAndLog() + + verify(storage).putFileContents(any(), written.capture()) + assertThat(JSONObject(written.firstValue).has("output")).isFalse() + } + + /** + * Parsing the text back is a failure mode that could not exist while the caller handed over + * objects. One bad document must cost that one file, not the whole worker run. + */ + @Test + fun `unparsable input fails without throwing out of the worker`() = runTest { + whenever(config.isEngineeringMode()).thenReturn(true) + whenever(dataInbox.drain(ApsExportInbox)).thenReturn(listOf(apsData(input = "not json"))) + + val result = worker().doWorkAndLog() + + assertIs(result) + verify(storage, never()).putFileContents(any(), any()) + } } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index 1a3205b056e6..b7ef9506eaa9 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -76,6 +76,7 @@ import app.aaps.core.objects.extensions.convertedToAbsolute import app.aaps.core.objects.extensions.convertedToPercent import app.aaps.core.objects.extensions.json import app.aaps.core.objects.extensions.plannedRemainingMinutes +import app.aaps.core.objects.extensions.with import app.aaps.core.ui.compose.icons.IcLoopClosed import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.plugins.aps.R @@ -92,6 +93,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import org.json.JSONObject import javax.inject.Inject import javax.inject.Provider @@ -946,36 +950,42 @@ class LoopPlugin @Inject constructor( aapsLogger.debug(LTag.NSCLIENT, "Building DeviceStatus for $reason") val profile = profileFunction.getProfile() ?: return - var apsResult: JSONObject? = null + var apsResult: JsonObject? = null var iob: JSONObject? = null - var enacted: JSONObject? = null + var enacted: JsonObject? = null lastRun?.let { lastRun -> if (lastRun.lastAPSRun > dateUtil.now() - 300 * 1000L) { // do not send if result is older than 1 min - apsResult = lastRun.request?.json()?.also { - it.put("timestamp", dateUtil.toISOString(lastRun.lastAPSRun)) - it.put("isfMgdlForCarbs", profile.getIsfMgdlForCarbs(dateUtil.now(), "LoopPlugin", config, processedDeviceStatusData)) + apsResult = lastRun.request?.json()?.with { + put("timestamp", dateUtil.toISOString(lastRun.lastAPSRun)) + put("isfMgdlForCarbs", profile.getIsfMgdlForCarbs(dateUtil.now(), "LoopPlugin", config, processedDeviceStatusData)) } iob = lastRun.request?.iob?.json(dateUtil)?.also { it.put("time", dateUtil.toISOString(lastRun.lastAPSRun)) } - val requested = JSONObject() // Snapshot the mutable field once: the APS loop (invoke()) can null/reassign // lastRun.tbrSetByPump concurrently, so re-dereferencing it with !! below raced and // threw NPE. A single read is also a consistent snapshot (it was read 3x before). val tbrSetByPump = lastRun.tbrSetByPump if (tbrSetByPump?.enacted == true) { // enacted - enacted = lastRun.request?.json()?.also { - it.put("rate", tbrSetByPump.json(profile.getBasal())["rate"]) - it.put("duration", tbrSetByPump.json(profile.getBasal())["duration"]) - it.put("received", true) + val pumpJson = tbrSetByPump.json(profile.getBasal()) + enacted = lastRun.request?.let { request -> + request.json()?.with { + // pumpJson is still an org.json document. get() throws on a missing entry + // exactly as before, and passing the Number through unchanged keeps whatever + // numeric type the pump result carried. + put("rate", pumpJson.get("rate") as Number) + put("duration", pumpJson.get("duration") as Number) + put("received", true) + putJsonObject("requested") { + put("duration", request.duration) + put("rate", request.rate) + put("temp", "absolute") + put("smb", request.smb) + } + put("smb", tbrSetByPump.bolusDelivered) + } } - requested.put("duration", lastRun.request?.duration) - requested.put("rate", lastRun.request?.rate) - requested.put("temp", "absolute") - requested.put("smb", lastRun.request?.smb) - enacted?.put("requested", requested) - enacted?.put("smb", tbrSetByPump.bolusDelivered) } } } diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt index 90253150d390..df415dcd333e 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt @@ -2,17 +2,22 @@ package app.aaps.plugins.aps.loop import android.app.NotificationManager import android.content.Context +import app.aaps.core.data.model.DS import app.aaps.core.data.model.RM import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.PumpDescription import app.aaps.core.data.time.T import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources +import app.aaps.core.interfaces.aps.APS +import app.aaps.core.interfaces.aps.APSResult +import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.UserEntryLogger import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData +import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.pump.PumpStatusProvider import app.aaps.core.interfaces.pump.PumpWithConcentration import app.aaps.core.interfaces.queue.CommandQueue @@ -20,19 +25,27 @@ import app.aaps.core.interfaces.receivers.ReceiverStatusStore import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.objects.constraints.ConstraintObject +import app.aaps.core.objects.profile.ProfileSealed import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.json.JSONException +import org.json.JSONObject import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -520,4 +533,118 @@ class LoopPluginTest : TestBaseWithProfile() { // endregion +// region buildAndStoreDeviceStatus + + /** + * The device-status payload uploaded to Nightscout had no coverage at all, which is thin for + * something written every loop cycle. It is also the one place that takes the APS document and adds + * to it, so it is where an immutable document can quietly lose fields: the old code wrote into the + * object returned by [app.aaps.core.interfaces.aps.APSResult.json] and relied on that write sticking. + * + * The APS result is stubbed rather than run, so these tests are about the envelope - which entries + * get added, and that the original ones survive - not about what the algorithm produced. + */ + private fun apsResultReturning(vararg entries: Pair): APSResult = + mock().also { result -> + whenever(result.json()).thenReturn(buildJsonObject { entries.forEach { (k, v) -> put(k, v) } }) + whenever(result.duration).thenReturn(30) + whenever(result.rate).thenReturn(1.5) + whenever(result.smb).thenReturn(0.4) + } + + private suspend fun prepareDeviceStatus(request: APSResult, tbrSetByPump: PumpEnactResult? = null) { + whenever(pumpStatusProvider.generatePumpJsonStatus()).thenReturn(JsonObject(emptyMap())) + // The profile reads isfMgdlForCarbs through the active APS and errors out when there is none. + // Stubbed in two steps: a whenever() nested inside another one confuses Mockito. + val aps = mock() + whenever(aps.usingDynamicIsf()).thenReturn(false) + whenever(activePlugin.activeAPS).thenReturn(aps) + // ProfileSealed reads activeAPS once, when it is built, so the base fixture's profile was built + // before the stub above existed. Build a fresh one - outside the thenReturn(), because that + // constructor call touches a mock and Mockito would read it as another unfinished stubbing. + val profile = ProfileSealed.EPS(effectiveProfileSwitch, activePlugin) + whenever(profileFunction.getProfile()).thenReturn(profile) + loopPlugin.lastRun = Loop.LastRun().apply { + this.request = request + this.lastAPSRun = dateUtil.now() + this.tbrSetByPump = tbrSetByPump + } + } + + private suspend fun storedDeviceStatus(): DS { + loopPlugin.buildAndStoreDeviceStatus("test") + val captor = argumentCaptor() + verify(persistenceLayer).insertDeviceStatus(captor.capture()) + return captor.firstValue + } + + @Test + fun `suggested keeps the aps entries and gains timestamp and isf`() = runTest { + prepareDeviceStatus(apsResultReturning("eventualBG" to 120, "carbsReq" to 0)) + + val suggested = JSONObject(storedDeviceStatus().suggested!!) + + // The document the APS produced is still all there… + assertThat(suggested.getInt("eventualBG")).isEqualTo(120) + assertThat(suggested.getInt("carbsReq")).isEqualTo(0) + // …and the two entries this method adds arrived. + assertThat(suggested.has("timestamp")).isTrue() + assertThat(suggested.has("isfMgdlForCarbs")).isTrue() + } + + /** No temp basal was set on the pump, so there is nothing enacted to report. */ + @Test + fun `enacted is absent when the pump enacted nothing`() = runTest { + prepareDeviceStatus(apsResultReturning("eventualBG" to 120)) + + assertThat(storedDeviceStatus().enacted).isNull() + } + + @Test + fun `enacted carries the pump rate and duration, the request and the delivered smb`() = runTest { + val tbr = pumpEnactResultProvider.get().enacted(true).isPercent(false).absolute(1.25).duration(45).bolusDelivered(0.0) + prepareDeviceStatus(apsResultReturning("eventualBG" to 120), tbrSetByPump = tbr) + + val enacted = JSONObject(storedDeviceStatus().enacted!!) + + assertThat(enacted.getInt("eventualBG")).isEqualTo(120) // the aps document survived the merge + assertThat(enacted.getDouble("rate")).isEqualTo(1.25) // …from the pump result, not the request + assertThat(enacted.getInt("duration")).isEqualTo(45) + assertThat(enacted.getBoolean("received")).isTrue() + assertThat(enacted.getDouble("smb")).isEqualTo(0.0) + // "requested" is a nested object holding what the APS asked for, next to what the pump did. + val requested = enacted.getJSONObject("requested") + assertThat(requested.getInt("duration")).isEqualTo(30) + assertThat(requested.getDouble("rate")).isEqualTo(1.5) + assertThat(requested.getString("temp")).isEqualTo("absolute") + assertThat(requested.getDouble("smb")).isEqualTo(0.4) + } + + /** + * A pump result that only delivered an SMB carries no rate/duration, and reading them throws. That + * predates the move to an immutable document - it is pinned here so the behaviour is at least known + * rather than discovered from a crash report. + */ + @Test + fun `an smb only pump result has no rate to report and throws`() = runTest { + val smbOnly = pumpEnactResultProvider.get().enacted(true).bolusDelivered(0.3) + prepareDeviceStatus(apsResultReturning("eventualBG" to 120), tbrSetByPump = smbOnly) + + assertThrows { loopPlugin.buildAndStoreDeviceStatus("test") } + } + + /** Older than 5 minutes: the run is stale, so neither document is sent. */ + @Test + fun `a stale run sends no suggested and no enacted`() = runTest { + prepareDeviceStatus(apsResultReturning("eventualBG" to 120)) + loopPlugin.lastRun?.lastAPSRun = dateUtil.now() - T.mins(6).msecs() + + val stored = storedDeviceStatus() + + assertThat(stored.suggested).isNull() + assertThat(stored.enacted).isNull() + } + +// endregion + } From a1ff557608258d71a6329a59530fa3d7b578f3f5 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 18:33:51 +0200 Subject: [PATCH 075/146] Declare androidx.collection 1.6.0 instead of resolving it transitively Nothing declared it, so it landed on 1.1.0 in some modules and 1.5.0 in others. Those are different generations of the same class: 1.4.0 rewrote it in Kotlin, where size is a property as well as a function. AutosensDataStore uses `.size` and 26 other call sites use `size()`, and both compile today only because they sit in modules that resolve differently. Moving a file between those modules would fail to build for a reason that does not look like the cause. From 1.4.0 the library is multiplatform, so LongSparseArray is usable from shared code and does not need replacing. That leaves AutosensDataStore, TddCalculator and TirCalculator with no remaining blocker. api, not implementation: LongSparseArray is in this module's own signatures, so consumers have to see the same version. No source changes - size(), keyAt, valueAt, append, removeAt, indexOfKey and putAll all compile unchanged, with no deprecation warnings. --- core/interfaces/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 6cc7889b2f2a..97485a0cb852 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -59,6 +59,9 @@ dependencies { api(libs.com.google.dagger.hilt.android) api(libs.androidx.appcompat) + // api, not implementation: LongSparseArray is part of this module's own API (AutosensDataStore, + // TddCalculator, TirCalculator all expose it), so consumers have to see the same version. + api(libs.androidx.collection) api(libs.androidx.compose.ui) api(libs.androidx.documentfile) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 579a634686eb..08b8c49c6f29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -108,6 +108,11 @@ androidx-activity = { group = "androidx.activity", name = "activity-ktx", versio androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.7.1" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version = "1.1.0" } androidx-browser = { group = "androidx.browser", name = "browser", version = "1.10.0" } +# Declared rather than left to transitive resolution: it used to land on 1.1.0 in some modules and +# 1.5.0 in others, and those are different generations of the same class (the newer one is a Kotlin +# rewrite where size is also a property), so the same code did not compile everywhere. From 1.4.0 it +# is multiplatform, which is what keeps LongSparseArray usable from shared code. +androidx-collection = { group = "androidx.collection", name = "collection", version = "1.6.0" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version = "2.2.2" } androidx-core = { group = "androidx.core", name = "core-ktx", version = "1.19.0" } androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastorePreferences" } From 9ba7d1a7e59608e7194c12fe5591890e2a6b6a7a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 18:56:07 +0200 Subject: [PATCH 076/146] Round without BigDecimal or InvalidParameterException Two JVM-only imports, both replaceable without changing what the functions return. java.security.InvalidParameterException is a JCA class and not what argument checking is for. It becomes require(), so IllegalArgumentException. Nothing caught it - every use of it in this repo is a throw - so the type change reaches no handler. The throw had no test; it has one now. BigDecimal was doing real work and could not simply go: a plain n * step differs from it in 8 of the 16 vectors the existing tests already cover (3 * 0.05 is 0.15000000000000002, 12 * 0.05 is 0.6000000000000001). Those are dose and rate values that travel into pump commands and the Nightscout payload. The same answer comes out of integer arithmetic. BigDecimal.valueOf(step) is exactly the decimal that step.toString() spells, so reading that text back as unscaled / 10^scale loses nothing, the multiply is an exact Long one, and the closing division is correctly rounded. Equal by construction, not by approximation - and the new tests sweep every step size a driver in this repo asks for against the old implementation, asserting exact equality rather than a tolerance. Two things the string parsing has to get right: below 1e-3 Double.toString returns scientific notation, so 0.0001 arrives as "1.0E-4" and the exponent has to fold into the scale. And a whole-number step has no fractional digits at all. Both are real, both are tested. One limit BigDecimal did not have, documented rather than guarded: the product is exact below 2^53, which at the deepest step in use allows values up to about 9e11. A hard check there could stop the loop over an input that is merely large. --- .../app/aaps/core/interfaces/utils/Round.kt | 52 +++++++-- .../objects/interfaces/utils/RoundTest.kt | 100 ++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt index 414d04b9bc06..55a475b2f737 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt @@ -1,10 +1,9 @@ package app.aaps.core.interfaces.utils -import java.math.BigDecimal -import java.security.InvalidParameterException import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor +import kotlin.math.pow import kotlin.math.roundToLong /** @@ -13,9 +12,12 @@ import kotlin.math.roundToLong object Round { fun roundTo(x: Double, step: Double): Double { - if (x.isNaN()) throw InvalidParameterException("Parameter is NaN") + // Was java.security.InvalidParameterException, which is a JCA class and not what argument + // checking is for. Nothing caught it - every use of it in this repo is a throw - so the type + // change reaches no handler. + require(!x.isNaN()) { "Parameter is NaN" } return if (x == 0.0) 0.0 - else BigDecimal.valueOf((x / step).roundToLong()).multiply(BigDecimal.valueOf(step)).toDouble() + else times(x = (x / step).roundToLong(), step = step) } fun floorTo(x: Double, step: Double): Double { @@ -27,7 +29,7 @@ object Round { // This never pushes the result above x, so the "must not deliver more than asked / // must not exceed the max constraint" invariant in SafetyPlugin still holds. val n = if (abs(q - q.roundToLong()) < 1e-9) q.roundToLong() else floor(q).toLong() - return BigDecimal.valueOf(n).multiply(BigDecimal.valueOf(step)).toDouble() + return times(x = n, step = step) } fun ceilTo(x: Double, step: Double): Double { @@ -38,9 +40,45 @@ object Round { // grid when the quotient is within tolerance of an integer, else ceil. Never pushes the // result below x, so callers that round UP to stay within a limit keep that guarantee. val n = if (abs(q - q.roundToLong()) < 1e-9) q.roundToLong() else ceil(q).toLong() - return BigDecimal.valueOf(n).multiply(BigDecimal.valueOf(step)).toDouble() + return times(x = n, step = step) + } + + /** + * `x * step`, giving the [Double] nearest to the **decimal** product. + * + * A plain `x * step` is not good enough here: `3 * 0.05` is `0.15000000000000002` and + * `12 * 0.05` is `0.6000000000000001`. Those are dose and rate values, so the extra digits + * travel into pump commands, comparisons and the Nightscout payload. + * + * This used to be `BigDecimal.valueOf(x).multiply(BigDecimal.valueOf(step)).toDouble()`, which is + * JVM only. The same answer comes out of integer arithmetic: `BigDecimal.valueOf(step)` is exactly + * the decimal that `step.toString()` spells, so reading that text back as `unscaled / 10^scale` + * loses nothing, the multiply is an exact [Long] one, and the single closing division is correctly + * rounded. So the result is the nearest double to the true decimal product - the same value the + * old code produced, by construction rather than by approximation. + * + * Limit worth knowing, which BigDecimal did not have: the product is exact while it stays under + * 2^53 (about 9.0e15), and it is roughly `|x * step| * 10^scale`. At the deepest step any pump + * uses (0.0001, so scale 4) that allows values up to about 9e11. Doses, rates and glucose values + * are far below it, so this is a note rather than a guard - a hard check here could stop the loop + * over an input that is merely large. + */ + private fun times(x: Long, step: Double): Double { + // Shortest round-trip text, the same source BigDecimal.valueOf(double) reads. Below 1e-3 it + // arrives in scientific notation ("1.0E-4" for 0.0001, a step that is really used), so the + // exponent has to be folded into the scale rather than assumed absent. + val text = step.toString() + val e = text.indexOf('E') + val mantissa = if (e < 0) text else text.substring(0, e) + val exponent = if (e < 0) 0 else text.substring(e + 1).toInt() + val point = mantissa.indexOf('.') + val unscaled = (if (point < 0) mantissa else mantissa.substring(0, point) + mantissa.substring(point + 1)).toLong() + val scale = (if (point < 0) 0 else mantissa.length - point - 1) - exponent + + val product = x * unscaled + return if (scale >= 0) product / 10.0.pow(scale) else product * 10.0.pow(-scale) } fun isSame(d1: Double, d2: Double): Boolean = abs(d1 - d2) <= 0.000001 -} \ No newline at end of file +} diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/RoundTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/RoundTest.kt index 70bd8fcccdc2..8a7f494ace74 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/RoundTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/utils/RoundTest.kt @@ -3,6 +3,10 @@ package app.aaps.core.objects.interfaces.utils import app.aaps.core.interfaces.utils.Round import com.google.common.truth.Truth.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import kotlin.math.ceil +import kotlin.math.floor class RoundTest { @@ -52,4 +56,100 @@ class RoundTest { fun isSameTest() { assertThat(Round.isSame(0.54, 0.54)).isTrue() } + + /** + * NaN reaching a dose rounding function means an upstream calculation already went wrong, so it + * has to be rejected rather than rounded into a number. The throw had no test at all. + */ + @Test + fun `roundTo rejects NaN`() { + assertThrows { Round.roundTo(Double.NaN, 0.05) } + } + + // region parity with the old BigDecimal implementation + + /** Exactly what Round did before: BigDecimal.valueOf(n).multiply(BigDecimal.valueOf(step)).toDouble(). */ + private fun oldWay(n: Long, step: Double): Double = + BigDecimal.valueOf(n).multiply(BigDecimal.valueOf(step)).toDouble() + + /** Every step size any driver in this repo actually asks for, plus 1.0 for whole units. */ + private val realSteps = listOf(0.0001, 0.001, 0.01, 0.025, 0.05, 0.1, 0.5, 1.0) + + /** + * The reason this can be swept rather than spot-checked: `times` claims to produce *the same + * double* as BigDecimal did, not merely a close one. A sweep over every real step size and a wide + * range of grid positions either holds exactly or does not. + * + * `isEqualTo`, deliberately - a tolerance here would hide precisely the digits this code exists to + * get rid of. + */ + @Test + fun `roundTo matches the old BigDecimal result exactly`() { + for (step in realSteps) + for (n in -2000..2000) { + val x = n * step + if (x == 0.0) continue + assertThat(Round.roundTo(x, step)).isEqualTo(oldWay(n.toLong(), step)) + } + } + + @Test + fun `floorTo and ceilTo match the old BigDecimal result exactly`() { + for (step in realSteps) + for (n in -2000..2000) { + val x = n * step + if (x == 0.0) continue + // On-grid input, so both directions land on n and the comparison is about the multiply. + assertThat(Round.floorTo(x, step)).isEqualTo(oldWay(n.toLong(), step)) + assertThat(Round.ceilTo(x, step)).isEqualTo(oldWay(n.toLong(), step)) + } + } + + /** + * Off-grid inputs, where floor and ceil pick different step counts. Keeps the sweep above from + * only ever exercising the snapping branch. + */ + @Test + fun `off grid values still match the old result in both directions`() { + for (step in realSteps) + for (n in -500..500) { + val x = n * step + step / 3.0 + if (x == 0.0) continue + assertThat(Round.floorTo(x, step)).isEqualTo(oldWay(floor(x / step).toLong(), step)) + assertThat(Round.ceilTo(x, step)).isEqualTo(oldWay(ceil(x / step).toLong(), step)) + } + } + + /** + * 0.0001 is below 1e-3, so Double.toString spells it "1.0E-4". Reading the scale by looking for a + * decimal point alone would get 1 instead of 4 and be out by a factor of a thousand. The step is + * genuinely used, so this is a real branch, not a hypothetical one. + */ + @Test + fun `a step written in scientific notation keeps its scale`() { + assertThat(0.0001.toString()).contains("E") // the premise, in case a toString ever changes + assertThat(Round.roundTo(0.0017, 0.0001)).isEqualTo(0.0017) + assertThat(Round.roundTo(0.00123456, 0.0001)).isEqualTo(0.0012) + assertThat(Round.floorTo(0.00019, 0.0001)).isEqualTo(0.0001) + } + + /** A whole-number step has no fractional digits at all - scale 0, which is its own branch. */ + @Test + fun `a whole number step works`() { + assertThat(Round.roundTo(7.4, 1.0)).isEqualTo(7.0) + assertThat(Round.roundTo(7.6, 1.0)).isEqualTo(8.0) + assertThat(Round.floorTo(7.9, 1.0)).isEqualTo(7.0) + assertThat(Round.ceilTo(7.1, 1.0)).isEqualTo(8.0) + } + + /** The digits this whole function exists to avoid. */ + @Test + fun `results carry no float noise`() { + assertThat(Round.roundTo(0.15, 0.05)).isEqualTo(0.15) // plain 3 * 0.05 is 0.15000000000000002 + assertThat(Round.roundTo(0.6, 0.05)).isEqualTo(0.6) // plain 12 * 0.05 is 0.6000000000000001 + assertThat(Round.roundTo(0.3, 0.1)).isEqualTo(0.3) // plain 3 * 0.1 is 0.30000000000000004 + assertThat(Round.roundTo(-3.2553715764602713, 0.01)).isEqualTo(-3.26) + } + + // endregion } From c42bde3538e86de22753fbeaecb3595bda32b31a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 20:42:10 +0200 Subject: [PATCH 077/146] Make :core:interfaces a multiplatform module Plugin swap to kotlin("multiplatform") + com.android.kotlin.multiplatform.library, and the 255 files split into commonMain (117) and androidMain (138). The four convention plugins are gone: they apply com.android.library, which AGP 9 refuses alongside the multiplatform plugin, so lint and the test dependencies are restated by hand the way :core:keys already does. Coverage needs no opt in - the aggregation detects a multiplatform module by src/commonMain and takes the android compilation. The split was NOT done by reading imports. That only finds direct platform imports and misses same package references and JVM constructs that need no import at all - @JvmField, Class<*>, synchronized {}, ReflectionToStringBuilder. Every file was placed by compiling for iosArm64 and moving whatever failed, repeated to a fixpoint, so commonMain is platform neutral as of this commit. The projection from reading imports was 221 common files; the compiler says 117. Only the Android target ships, and the reason is not the split. The compose compiler plugin is applied per project rather than per target and fails any compilation with no Compose runtime on the class path, a plain jvm() as much as an Apple one. This module needs it for exactly one declaration, UserEntryPresentationHelper.iconColor. Moving that interface to :core:ui - both consumers already depend on it - drops the plugin and opens the other targets, but it changes another module's API so it is left as its own change. Without a native target the purity above stops being enforced; that is written into the build file rather than left implicit. OpenForTesting collapses to one commonMain file. The debug and release copies differed only by an @OpenClass meta annotation that nothing read - allOpen is keyed on the annotation's own name - so the variant split did nothing, and classes were already open in release builds. Also included, because PureProfile carries both changes and they cannot be separated without an intermediate commit that does not compile: profile time zones are DST aware. PureProfile held a java.util.TimeZone and only rawOffset was ever read, which is the zone's standard offset, so Europe/Prague reported +01:00 in July. The consumer turns that number back into a zone name by looking for a zone at that offset now, and in July nothing in Europe is at +01:00, so the Nightscout profile named an unrelated zone. It holds a resolved offset now, and there are tests for both halves of the year. Tested: assembleFullDebug, assembleFullRelease, testFullDebugUnitTest, jvmTest, testAndroidHostTest (61 tests in this module), :app:assembleFullDebugAndroidTest and jacocoAllDebugReport. Not run on a device. Note for anyone pulling this: the module needs a clean build directory. Old intermediates from the android library layout produce a cascade of bogus unresolved reference errors. --- core/interfaces/build.gradle.kts | 177 ++++++++++++------ .../interfaces/aps/RtIsoStringParityTest.kt | 0 .../db/ClockSkewCompensationTest.kt | 0 .../insulin/ConcentrationTypeTest.kt | 0 .../interfaces/pump/BolusProgressDataTest.kt | 0 .../core/interfaces/pump/PumpInsulinTest.kt | 0 .../aaps/core/interfaces/pump/PumpRateTest.kt | 0 .../interfaces/rx/weardata/EventDataTest.kt | 0 .../rx/weardata/LoopStatusDataTest.kt | 0 .../TempTargetPresetExtensionsTest.kt | 0 .../core/interfaces/utils/SafeParseTest.kt | 0 .../{main => androidMain}/AndroidManifest.xml | 0 .../app/aaps/core/interfaces/aps/APS.kt | 0 .../app/aaps/core/interfaces/aps/APSResult.kt | 0 .../core/interfaces/aps/AutosensDataStore.kt | 0 .../app/aaps/core/interfaces/aps/Loop.kt | 0 .../kotlin/app/aaps/core/interfaces/aps/RT.kt | 0 .../aaps/core/interfaces/aps/Sensitivity.kt | 0 .../core/interfaces/automation/Automation.kt | 0 .../interfaces/automation/AutomationEvent.kt | 0 .../core/interfaces/bolus/BatchExecutor.kt | 0 .../interfaces/bolus/WizardBolusExecutor.kt | 0 .../core/interfaces/bolus/WizardExecutor.kt | 0 .../clientcontrol/ActionProgress.kt | 0 .../ClientControlActionDispatcher.kt | 0 .../interfaces/clientcontrol/PendingAction.kt | 0 .../interfaces/configuration/ConfigBuilder.kt | 0 .../constraints/ConstraintsChecker.kt | 0 .../constraints/PluginConstraints.kt | 0 .../constraints/PumpPluginConstraints.kt | 0 .../interfaces/db/ClockSkewCompensation.kt | 0 .../core/interfaces/db/PersistenceLayer.kt | 0 .../kotlin/app/aaps/core/interfaces/di/APS.kt | 0 .../app/aaps/core/interfaces/di/AllConfigs.kt | 0 .../core/interfaces/di/ApplicationScope.kt | 0 .../aaps/core/interfaces/di/NotNSClient.kt | 0 .../app/aaps/core/interfaces/di/PumpDriver.kt | 0 .../interfaces/insulin/ConcentrationHelper.kt | 0 .../core/interfaces/insulin/InsulinManager.kt | 0 .../core/interfaces/insulin/InsulinType.kt | 0 .../core/interfaces/iob/IobCobCalculator.kt | 0 .../maintenance/CloudDirectoryManager.kt | 0 .../maintenance/CloudStorageProvider.kt | 0 .../maintenance/FileListProvider.kt | 0 .../maintenance/ImportExportPrefs.kt | 0 .../interfaces/maintenance/Maintenance.kt | 0 .../interfaces/maintenance/PrefMetadata.kt | 0 .../aaps/core/interfaces/maintenance/Prefs.kt | 0 .../core/interfaces/maintenance/PrefsFile.kt | 0 .../maintenance/PrefsMetadataKey.kt | 0 .../interfaces/maintenance/PrefsStatus.kt | 0 .../notifications/AlarmSoundPlayer.kt | 0 .../notifications/NotificationHolder.kt | 0 .../notifications/NotificationManager.kt | 0 .../nsclient/ProcessedDeviceStatusData.kt | 0 .../core/interfaces/plugin/ActivePlugin.kt | 0 .../core/interfaces/plugin/PermissionGroup.kt | 0 .../interfaces/plugin/PermissionProvider.kt | 0 .../aaps/core/interfaces/plugin/PluginBase.kt | 0 .../plugin/PluginBaseWithPreferences.kt | 0 .../interfaces/plugin/PluginDescription.kt | 0 .../interfaces/profile/EffectiveProfile.kt | 0 .../aaps/core/interfaces/profile/Profile.kt | 0 .../interfaces/profile/ProfileFunction.kt | 0 .../interfaces/profile/ProfileRepository.kt | 0 .../core/interfaces/profile/ProfileStore.kt | 0 .../core/interfaces/profile/ProfileUtil.kt | 0 .../profile/ProfileValidationError.kt | 0 .../core/interfaces/profile/PureProfile.kt | 37 ++++ .../core/interfaces/profile/SingleProfile.kt | 0 .../interfaces/protection/PasswordCheck.kt | 0 .../aaps/core/interfaces/pump/BlePreCheck.kt | 0 .../core/interfaces/pump/BolusProgressData.kt | 0 .../core/interfaces/pump/DetailedBolusInfo.kt | 0 .../pump/DetailedBolusInfoStorage.kt | 0 .../core/interfaces/pump/MappedStateFlow.kt | 0 .../app/aaps/core/interfaces/pump/Pump.kt | 0 .../aaps/core/interfaces/pump/PumpInsulin.kt | 0 .../core/interfaces/pump/PumpPluginBase.kt | 0 .../aaps/core/interfaces/pump/PumpProfile.kt | 0 .../app/aaps/core/interfaces/pump/PumpRate.kt | 0 .../app/aaps/core/interfaces/pump/PumpSync.kt | 0 .../interfaces/pump/PumpWithConcentration.kt | 0 .../interfaces/pump/TemporaryBasalStorage.kt | 0 .../interfaces/pump/actions/CustomAction.kt | 0 .../pump/actions/CustomActionType.kt | 0 .../interfaces/pump/rfcomm/RfcommTransport.kt | 0 .../aaps/core/interfaces/queue/Callback.kt | 0 .../app/aaps/core/interfaces/queue/Command.kt | 0 .../core/interfaces/queue/CommandQueue.kt | 0 .../core/interfaces/queue/CustomCommand.kt | 0 .../interfaces/resources/ResourceHelper.kt | 0 .../aaps/core/interfaces/rx/AapsSchedulers.kt | 0 .../app/aaps/core/interfaces/rx/bus/RxBus.kt | 0 .../aaps/core/interfaces/rx/events/Event.kt | 0 .../rx/events/EventAPSCalculationFinished.kt | 0 .../rx/events/EventAcceptOpenLoopChange.kt | 0 .../core/interfaces/rx/events/EventAppExit.kt | 0 .../rx/events/EventAppInitialized.kt | 0 .../EventAutosensCalculationFinished.kt | 0 .../interfaces/rx/events/EventBTChange.kt | 0 .../rx/events/EventBucketedDataCreated.kt | 0 .../rx/events/EventCalibrationChanged.kt | 0 .../rx/events/EventConcentrationChange.kt | 0 .../rx/events/EventConfigBuilderChange.kt | 0 .../rx/events/EventCustomActionsChanged.kt | 0 .../rx/events/EventDiaconnG8PumpLogReset.kt | 0 .../rx/events/EventInitializationChanged.kt | 0 .../core/interfaces/rx/events/EventLoop.kt | 0 .../rx/events/EventLoopUpdateGui.kt | 0 .../interfaces/rx/events/EventMobileToWear.kt | 0 .../rx/events/EventMobileToWearWatchface.kt | 0 .../rx/events/EventNewOpenLoopNotification.kt | 0 .../rx/events/EventNsClientStatusUpdated.kt | 0 .../interfaces/rx/events/EventNtpStatus.kt | 0 .../rx/events/EventProfileChangeRequested.kt | 0 .../rx/events/EventPumpStatusChanged.kt | 0 .../interfaces/rx/events/EventQueueChanged.kt | 0 .../rx/events/EventRefreshButtonState.kt | 0 .../rx/events/EventRefreshOverview.kt | 0 .../interfaces/rx/events/EventSWRLStatus.kt | 0 .../interfaces/rx/events/EventSWSyncStatus.kt | 0 .../interfaces/rx/events/EventSWUpdate.kt | 0 .../interfaces/rx/events/EventShowDialog.kt | 0 .../interfaces/rx/events/EventShowSnackbar.kt | 0 .../core/interfaces/rx/events/EventStatus.kt | 0 .../interfaces/rx/events/EventUpdateGui.kt | 0 .../events/EventUpdateOverviewCalcProgress.kt | 0 .../rx/events/EventUpdateSelectedWatchface.kt | 0 .../rx/events/EventWearDataToMobile.kt | 0 .../interfaces/rx/events/EventWearToMobile.kt | 0 .../rx/events/EventWearUpdateGui.kt | 0 .../rx/events/EventWearUpdateTiles.kt | 0 .../core/interfaces/rx/weardata/EventData.kt | 0 .../core/interfaces/scenes/SceneActions.kt | 0 .../interfaces/scenes/SceneAutomationApi.kt | 0 .../interfaces/scenes/SceneIconResolver.kt | 0 .../core/interfaces/sharedPreferences/SP.kt | 0 .../core/interfaces/smsCommunicator/Sms.kt | 0 .../smsCommunicator/SmsCommunicator.kt | 0 .../aaps/core/interfaces/storage/Storage.kt | 0 .../aaps/core/interfaces/ui/UiInteraction.kt | 0 .../userEntry/UserEntryPresentationHelper.kt | 0 .../aaps/core/interfaces/utils/DateUtil.kt | 0 .../core/interfaces/utils/DecimalFormatter.kt | 0 .../core/interfaces/utils/MidnightTime.kt | 0 .../core/interfaces/utils/TrendCalculator.kt | 0 .../interfaces/utils/fabric/FabricPrivacy.kt | 0 .../interfaces/workflow/CalculationSignals.kt | 0 .../workflow/CalculationWorkflow.kt | 0 .../res/values-ar-rSA/strings.xml | 0 .../res/values-bg-rBG/strings.xml | 0 .../res/values-ca-rES/strings.xml | 0 .../res/values-cs-rCZ/strings.xml | 0 .../res/values-da-rDK/strings.xml | 0 .../res/values-de-rDE/strings.xml | 0 .../res/values-el-rGR/strings.xml | 0 .../res/values-es-rES/strings.xml | 0 .../res/values-fr-rFR/strings.xml | 0 .../res/values-hr-rHR/strings.xml | 0 .../res/values-hu-rHU/strings.xml | 0 .../res/values-it-rIT/strings.xml | 0 .../res/values-iw-rIL/strings.xml | 0 .../res/values-ko-rKR/strings.xml | 0 .../res/values-lt-rLT/strings.xml | 0 .../res/values-nb-rNO/strings.xml | 0 .../res/values-nl-rNL/strings.xml | 0 .../res/values-pl-rPL/strings.xml | 0 .../res/values-pt-rBR/strings.xml | 0 .../res/values-pt-rPT/strings.xml | 0 .../res/values-ro-rRO/strings.xml | 0 .../res/values-ru-rRU/strings.xml | 0 .../res/values-sk-rSK/strings.xml | 0 .../res/values-sr-rCS/strings.xml | 0 .../res/values-sv-rSE/strings.xml | 0 .../res/values-tr-rTR/strings.xml | 0 .../res/values-uk-rUA/strings.xml | 0 .../res/values-vi-rVN/strings.xml | 0 .../res/values-zh-rCN/strings.xml | 0 .../res/values-zh-rTW/strings.xml | 0 .../res/values/strings.xml | 0 .../res/values/wear_paths.xml | 0 .../app/aaps/annotations/OpenForTesting.kt | 17 ++ .../core/interfaces/alerts/LocalAlertUtils.kt | 0 .../aaps/core/interfaces/aps/AutosensData.kt | 0 .../core/interfaces/aps/AutosensResult.kt | 0 .../aaps/core/interfaces/aps/CurrentTemp.kt | 0 .../aaps/core/interfaces/aps/GlucoseStatus.kt | 0 .../interfaces/aps/GlucoseStatusAutoIsf.kt | 0 .../core/interfaces/aps/GlucoseStatusSMB.kt | 0 .../app/aaps/core/interfaces/aps/IobTotal.kt | 0 .../app/aaps/core/interfaces/aps/MealData.kt | 0 .../aaps/core/interfaces/aps/OapsProfile.kt | 0 .../core/interfaces/aps/OapsProfileAutoIsf.kt | 0 .../aaps/core/interfaces/aps/Predictions.kt | 0 .../aaps/core/interfaces/autotune/Autotune.kt | 0 .../bgQualityCheck/BgQualityCheck.kt | 0 .../aaps/core/interfaces/bolus/BatchAction.kt | 0 .../interfaces/calibration/AddEntryResult.kt | 0 .../interfaces/calibration/Calibration.kt | 0 .../calibration/CalibrationContext.kt | 0 .../core/interfaces/configuration/Config.kt | 0 .../configuration/RunningConfigurationKeys.kt | 0 .../core/interfaces/constraints/Constraint.kt | 0 .../core/interfaces/constraints/Objectives.kt | 0 .../core/interfaces/constraints/Safety.kt | 0 .../core/interfaces/db/ProcessedTbrEbData.kt | 0 .../app/aaps/core/interfaces/dst/DstHelper.kt | 0 .../app/aaps/core/interfaces/graph/Scale.kt | 0 .../aaps/core/interfaces/graph/SeriesData.kt | 0 .../interfaces/insulin/ConcentrationType.kt | 0 .../interfaces/iob/GlucoseStatusProvider.kt | 0 .../local/LocaleDependentSetting.kt | 0 .../core/interfaces/logging/AAPSLogger.kt | 0 .../app/aaps/core/interfaces/logging/L.kt | 0 .../app/aaps/core/interfaces/logging/LTag.kt | 0 .../core/interfaces/logging/LogElement.kt | 0 .../core/interfaces/logging/LoggerUtils.kt | 0 .../interfaces/logging/UserEntryLogger.kt | 0 .../interfaces/maintenance/CloudModels.kt | 0 .../interfaces/navigation/ElementCategory.kt | 0 .../core/interfaces/navigation/ElementType.kt | 0 .../notifications/AapsNotification.kt | 0 .../interfaces/notifications/AlarmIntent.kt | 0 .../interfaces/notifications/AlarmSound.kt | 0 .../notifications/NotificationAction.kt | 0 .../notifications/NotificationCategory.kt | 0 .../notifications/NotificationHandle.kt | 0 .../notifications/NotificationId.kt | 0 .../notifications/NotificationLevel.kt | 0 .../aaps/core/interfaces/nsclient/NSAlarm.kt | 0 .../core/interfaces/nsclient/NSClientLog.kt | 0 .../interfaces/nsclient/NSClientRepository.kt | 0 .../interfaces/nsclient/StoreDataForDb.kt | 0 .../core/interfaces/overview/LastBgData.kt | 0 .../core/interfaces/overview/OverviewData.kt | 0 .../overview/graph/AapsClientStatusData.kt | 0 .../overview/graph/CalculationResults.kt | 0 .../overview/graph/GraphConfiguration.kt | 0 .../overview/graph/OverviewDataCache.kt | 0 .../interfaces/plugin/OwnDatabasePlugin.kt | 0 .../core/interfaces/profiling/Profiler.kt | 0 .../protection/ExportPasswordDataStore.kt | 0 .../interfaces/protection/ProtectionCheck.kt | 0 .../interfaces/protection/SecureEncrypt.kt | 0 .../core/interfaces/pump/BlePreCheckResult.kt | 0 .../app/aaps/core/interfaces/pump/Dana.kt | 0 .../app/aaps/core/interfaces/pump/Diaconn.kt | 0 .../app/aaps/core/interfaces/pump/Insight.kt | 0 .../app/aaps/core/interfaces/pump/Medtrum.kt | 0 .../aaps/core/interfaces/pump/OmnipodDash.kt | 0 .../aaps/core/interfaces/pump/OmnipodEros.kt | 0 .../core/interfaces/pump/PumpEnactResult.kt | 0 .../interfaces/pump/PumpStatusProvider.kt | 0 .../aaps/core/interfaces/pump/VirtualPump.kt | 0 .../core/interfaces/pump/ble/BleTransport.kt | 0 .../pump/defs/PumpDescriptionExtension.kt | 0 .../interfaces/pump/defs/PumpDeviceState.kt | 0 .../interfaces/pump/defs/PumpTypeExtension.kt | 0 .../aaps/core/interfaces/receivers/Intents.kt | 0 .../receivers/ReceiverStatusStore.kt | 0 .../core/interfaces/rx/ResilientCollect.kt | 0 .../core/interfaces/rx/weardata/CwfData.kt | 0 .../core/interfaces/rx/weardata/CwfFile.kt | 0 .../interfaces/rx/weardata/CwfMetaDataKey.kt | 0 .../interfaces/rx/weardata/LoopStatusData.kt | 0 .../core/interfaces/rx/weardata/ResData.kt | 0 .../core/interfaces/rx/weardata/ResFormat.kt | 0 .../core/interfaces/scenes/ActiveSceneSync.kt | 0 .../scenes/ClientControlSendResult.kt | 0 .../interfaces/scenes/SceneChainResolver.kt | 0 .../aaps/core/interfaces/scenes/SceneStore.kt | 0 .../app/aaps/core/interfaces/scenes/Scenes.kt | 0 .../core/interfaces/smoothing/Smoothing.kt | 0 .../aaps/core/interfaces/source/BgSource.kt | 0 .../core/interfaces/source/DexcomBoyda.kt | 0 .../core/interfaces/source/NSClientSource.kt | 0 .../core/interfaces/source/XDripSource.kt | 0 .../aaps/core/interfaces/stats/DexcomTIR.kt | 0 .../interfaces/stats/DexcomTirCalculator.kt | 0 .../app/aaps/core/interfaces/stats/TIR.kt | 0 .../core/interfaces/stats/TddCalculator.kt | 0 .../core/interfaces/stats/TirCalculator.kt | 0 .../core/interfaces/sync/DataSyncSelector.kt | 0 .../interfaces/sync/DataSyncSelectorXdrip.kt | 0 .../app/aaps/core/interfaces/sync/NsClient.kt | 0 .../app/aaps/core/interfaces/sync/Sync.kt | 0 .../app/aaps/core/interfaces/sync/Tidepool.kt | 0 .../core/interfaces/sync/XDripBroadcast.kt | 0 .../tempTargets/TempTargetPresetExtensions.kt | 0 .../aaps/core/interfaces/ui/IconsProvider.kt | 0 .../aaps/core/interfaces/utils/HardLimits.kt | 0 .../app/aaps/core/interfaces/utils/Round.kt | 0 .../aaps/core/interfaces/utils/SafeParse.kt | 0 .../aaps/core/interfaces/utils/TimeDiff.kt | 0 .../aaps/core/interfaces/utils/Translator.kt | 0 .../versionChecker/VersionCheckerUtils.kt | 0 .../versionChecker/VersionDefinition.kt | 0 .../core/interfaces/widget/WidgetUpdater.kt | 0 .../app/aaps/annotations/OpenForTesting.kt | 15 -- .../core/interfaces/profile/PureProfile.kt | 24 --- .../app/aaps/annotations/OpenForTesting.kt | 8 - .../extensions/ProfileSwitchExtension.kt | 17 +- .../core/objects/profile/ProfileSealed.kt | 11 +- .../objects/profile/ProfileTimeZoneTest.kt | 109 +++++++++++ .../plugins/aps/autotune/AutotuneCoreTest.kt | 7 +- .../plugins/aps/autotune/AutotunePrepTest.kt | 7 +- 307 files changed, 308 insertions(+), 121 deletions(-) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensationTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/insulin/ConcentrationTypeTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/pump/PumpInsulinTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/pump/PumpRateTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/rx/weardata/EventDataTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusDataTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt (100%) rename core/interfaces/src/{test => androidHostTest}/kotlin/app/aaps/core/interfaces/utils/SafeParseTest.kt (100%) rename core/interfaces/src/{main => androidMain}/AndroidManifest.xml (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/APS.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/APSResult.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/Loop.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/RT.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/automation/Automation.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/di/APS.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/di/AllConfigs.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/di/ApplicationScope.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/di/NotNSClient.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/di/PumpDriver.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/FileListProvider.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/Maintenance.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/Prefs.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefsMetadataKey.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/Profile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt (100%) create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/Pump.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/pump/rfcomm/RfcommTransport.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/queue/Callback.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/queue/Command.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/AapsSchedulers.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/Event.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/storage/Storage.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/utils/fabric/FabricPrivacy.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt (100%) rename core/interfaces/src/{main => androidMain}/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt (100%) rename core/interfaces/src/{main => androidMain}/res/values-ar-rSA/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-bg-rBG/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-ca-rES/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-cs-rCZ/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-da-rDK/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-de-rDE/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-el-rGR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-es-rES/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-fr-rFR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-hr-rHR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-hu-rHU/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-it-rIT/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-iw-rIL/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-ko-rKR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-lt-rLT/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-nb-rNO/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-nl-rNL/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-pl-rPL/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-pt-rBR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-pt-rPT/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-ro-rRO/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-ru-rRU/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-sk-rSK/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-sr-rCS/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-sv-rSE/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-tr-rTR/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-uk-rUA/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-vi-rVN/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-zh-rCN/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values-zh-rTW/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values/strings.xml (100%) rename core/interfaces/src/{main => androidMain}/res/values/wear_paths.xml (100%) create mode 100644 core/interfaces/src/commonMain/kotlin/app/aaps/annotations/OpenForTesting.kt rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/alerts/LocalAlertUtils.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/AutosensData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/AutosensResult.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/CurrentTemp.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/GlucoseStatus.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusAutoIsf.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusSMB.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/IobTotal.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/MealData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/OapsProfile.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/OapsProfileAutoIsf.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/aps/Predictions.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/autotune/Autotune.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/bgQualityCheck/BgQualityCheck.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/bolus/BatchAction.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/calibration/AddEntryResult.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/calibration/Calibration.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/calibration/CalibrationContext.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/configuration/Config.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/configuration/RunningConfigurationKeys.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/constraints/Constraint.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/constraints/Objectives.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/constraints/Safety.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/db/ProcessedTbrEbData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/dst/DstHelper.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/graph/Scale.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/graph/SeriesData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/iob/GlucoseStatusProvider.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/local/LocaleDependentSetting.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/AAPSLogger.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/L.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/LTag.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/LogElement.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/LoggerUtils.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/logging/UserEntryLogger.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/maintenance/CloudModels.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/navigation/ElementCategory.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/navigation/ElementType.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationCategory.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationHandle.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationLevel.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/nsclient/NSAlarm.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/nsclient/StoreDataForDb.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/LastBgData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/OverviewData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/graph/AapsClientStatusData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/graph/CalculationResults.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/graph/GraphConfiguration.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/overview/graph/OverviewDataCache.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/plugin/OwnDatabasePlugin.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/profiling/Profiler.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/protection/SecureEncrypt.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/BlePreCheckResult.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/Dana.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/Diaconn.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/Insight.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/Medtrum.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/OmnipodDash.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/OmnipodEros.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpEnactResult.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/VirtualPump.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/defs/PumpDeviceState.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/receivers/Intents.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/receivers/ReceiverStatusStore.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/CwfData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/CwfFile.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/ResData.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/ResFormat.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/scenes/ActiveSceneSync.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/scenes/ClientControlSendResult.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/scenes/SceneChainResolver.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/scenes/SceneStore.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/scenes/Scenes.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/smoothing/Smoothing.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/source/BgSource.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/source/DexcomBoyda.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/source/NSClientSource.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/source/XDripSource.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/stats/DexcomTirCalculator.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/stats/TIR.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/stats/TddCalculator.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/stats/TirCalculator.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/DataSyncSelectorXdrip.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/NsClient.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/Sync.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/Tidepool.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/sync/XDripBroadcast.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/ui/IconsProvider.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/utils/HardLimits.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/utils/Round.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/utils/SafeParse.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/utils/Translator.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/versionChecker/VersionCheckerUtils.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt (100%) rename core/interfaces/src/{main => commonMain}/kotlin/app/aaps/core/interfaces/widget/WidgetUpdater.kt (100%) delete mode 100644 core/interfaces/src/debug/kotlin/app/aaps/annotations/OpenForTesting.kt delete mode 100644 core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt delete mode 100644 core/interfaces/src/release/kotlin/app/aaps/annotations/OpenForTesting.kt create mode 100644 core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileTimeZoneTest.kt diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 97485a0cb852..b7ddb405c198 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -1,80 +1,135 @@ -import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min plugins { - alias(libs.plugins.android.library) + kotlin("multiplatform") + // NOT com.android.library. AGP 9 refuses that plugin together with the multiplatform plugin: + // "The 'com.android.library' (or 'com.android.application') plugin is not compatible with the + // 'org.jetbrains.kotlin.multiplatform' plugin since AGP 9.0." + alias(libs.plugins.android.kmp.library) alias(libs.plugins.compose.compiler) id("kotlinx-serialization") - id("android-module-dependencies") - id("test-module-dependencies") - id("compose-test-module-dependencies") - id("jacoco-module-dependencies") } -android { +// One task, not one per variant. A multiplatform module has no product flavours, and a Kotlin source +// set takes a task provider directly, so the Android variant API this used to go through is not +// needed. Same generator as :core:keys and :core:ui, pointed at this module's strings: it lets the +// data enums here (ConcentrationType, InsulinType, CwfMetadataKey) carry a TextRef instead of an +// R.string Int. The strings themselves do not move, and AAPT keeps resolving them as before. +val generateInterfacesStrings = tasks.register("generateInterfacesStrings") { + resDir.set(layout.projectDirectory.dir("src/androidMain/res")) + packageName.set("app.aaps.core.interfaces") + owner.set("interfaces") + objectName.set("InterfacesStrings") + idsObjectName.set("InterfacesStringIds") + reportFile.set(layout.buildDirectory.file("reports/interfacesStrings/translations.txt")) + // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the task + // name, so both properties would land on one directory and the second file written would delete + // the first. + commonOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/android")) +} - namespace = "app.aaps.core.interfaces" - defaultConfig { - minSdk = min(Versions.minSdk, Versions.wearMinSdk) - } +kotlin { + android { + namespace = "app.aaps.core.interfaces" + compileSdk = Versions.compileSdk + minSdk = min(Versions.minSdk, Versions.wearMinSdk) // Compatible with wear module + // Off by default for a multiplatform library, unlike a plain android library. + androidResources { enable = true } + // Creates the androidHostTest compilation, which also pulls in commonTest. + withHostTest { } + compilerOptions { jvmTarget.set(Versions.jvmTarget) } - buildFeatures { - compose = true + // Restated from android-module-dependencies, which this module can no longer apply. Without + // it MissingTranslation would switch on for the first time here and the locale files that are + // empty today would fail a release build. + lint { + checkReleaseBuilds = false + disable += "MissingTranslation" + disable += "ExtraTranslation" + } } -} -// Same generator as :core:keys and :core:ui, pointed at this module's strings. It lets the data -// enums here (ConcentrationType, InsulinType, CwfMetadataKey) carry a TextRef instead of an R.string -// Int, so they stop being Android-only. The strings themselves do not move, and AAPT keeps resolving -// them on Android exactly as before. -extensions.configure("androidComponents") { - onVariants { variant -> - val taskProvider = tasks.register( - "generate${variant.name.replaceFirstChar { it.uppercase() }}InterfacesStrings", - GenerateKeyStringsTask::class.java - ) { - resDir.set(layout.projectDirectory.dir("src/main/res")) - packageName.set("app.aaps.core.interfaces") - owner.set("interfaces") - objectName.set("InterfacesStrings") - idsObjectName.set("InterfacesStringIds") - reportFile.set(layout.buildDirectory.file("reports/interfacesStrings/${variant.name}-translations.txt")) - // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the - // task name, so both properties would land on one directory and the second file written - // would delete the first. - commonOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/${variant.name}/common")) - androidOutputDir.set(layout.buildDirectory.dir("generated/interfacesStrings/${variant.name}/android")) + // Only the Android target for now. Not because of the source split - see below. + // + // The compose compiler plugin is applied per project, not per target, and it fails ANY + // compilation that has no Compose runtime on the class path - a plain jvm() target as much as an + // Apple one. This module needs that plugin for exactly one declaration: + // `UserEntryPresentationHelper.iconColor` is `@Composable`. Every other Compose reference here is + // a plain type (ImageVector, Color, AnnotatedString) and needs only the dependency. + // + // So the way to open the other targets is to move that one interface to :core:ui - both of its + // consumers, :implementation and :ui, already depend on it - and drop the plugin from here. That + // touches another module's API, so it is deliberately left as its own change rather than folded + // into the source split. + // + // The split itself was done against the Apple compiler: every file in commonMain was placed by + // compiling for iosArm64 and moving whatever failed, so commonMain is platform neutral as of this + // commit. What is missing without that target is the *enforcement* - a java.* import added to + // commonMain later would compile fine on Android and nothing would object. + + sourceSets { + commonMain { + kotlin.srcDir(generateInterfacesStrings.flatMap { it.commonOutputDir }) + dependencies { + api(project(":core:data")) + api(project(":core:keys")) + + // project.dependencies.platform, because a Kotlin source set dependency block has no + // platform() of its own. + api(project.dependencies.platform(libs.kotlinx.serialization.bom)) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.serialization.protobuf) + api(libs.kotlinx.datetime) + api(project.dependencies.platform(libs.kotlinx.coroutines.bom)) + api(libs.kotlinx.coroutines.core) + // Multiplatform since 1.4.0, so LongSparseArray is usable from common code. + // AutosensDataStore, TddCalculator and TirCalculator all expose it, so it stays api. + api(libs.androidx.collection) + } } - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) - } -} -dependencies { - implementation(project(":core:data")) - api(project(":core:keys")) + androidMain { + // Android only: the string name to R.string id map. + kotlin.srcDir(generateInterfacesStrings.flatMap { it.androidOutputDir }) + dependencies { + // Everything here was `api` on the old android library and the 41 consumer modules + // resolve these transitively, so they must stay exported. They are Android or JVM + // only, which is exactly why they belong to this source set rather than commonMain. - // Dependency Injection - api(libs.com.google.dagger.android) - api(libs.com.google.dagger.hilt.android) + // Dependency Injection + api(libs.com.google.dagger.android) + api(libs.com.google.dagger.hilt.android) - api(libs.androidx.appcompat) - // api, not implementation: LongSparseArray is part of this module's own API (AutosensDataStore, - // TddCalculator, TirCalculator all expose it), so consumers have to see the same version. - api(libs.androidx.collection) - api(libs.androidx.compose.ui) - api(libs.androidx.documentfile) + api(libs.androidx.appcompat) + api(libs.androidx.compose.ui) + api(libs.androidx.documentfile) - api(platform(libs.kotlinx.serialization.bom)) - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.serialization.protobuf) + api(libs.org.apache.commons.lang3) + api(libs.net.danlew.android.joda) - api(libs.org.apache.commons.lang3) - api(libs.net.danlew.android.joda) - api(libs.kotlinx.datetime) + //RxBus / RxJava base + api(libs.io.reactivex.rxjava3.rxkotlin) + } + } - //RxBus / RxJava base - api(libs.io.reactivex.rxjava3.rxkotlin) + // Hand written rather than taken from test-module-dependencies, because that convention + // plugin applies com.android.library and so cannot be used here. + getByName("androidHostTest") { + dependencies { + implementation(libs.org.junit.jupiter) + implementation(libs.org.junit.jupiter.api) + implementation(libs.com.google.truth) + implementation(libs.org.mockito.kotlin) + implementation(libs.org.mockito.junit.jupiter) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.io.reactivex.rxjava3.rxandroid) + runtimeOnly(libs.org.junit.platform.launcher) + } + } + } +} - testImplementation(libs.io.reactivex.rxjava3.rxandroid) -} \ No newline at end of file +tasks.withType { + useJUnitPlatform() +} diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/aps/RtIsoStringParityTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensationTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensationTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensationTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensationTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/insulin/ConcentrationTypeTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/insulin/ConcentrationTypeTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/insulin/ConcentrationTypeTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/insulin/ConcentrationTypeTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/BolusProgressDataTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/PumpInsulinTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/PumpInsulinTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/PumpInsulinTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/PumpInsulinTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/PumpRateTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/PumpRateTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/pump/PumpRateTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/pump/PumpRateTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/rx/weardata/EventDataTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/weardata/EventDataTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/rx/weardata/EventDataTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/weardata/EventDataTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusDataTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusDataTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusDataTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusDataTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensionsTest.kt diff --git a/core/interfaces/src/test/kotlin/app/aaps/core/interfaces/utils/SafeParseTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/utils/SafeParseTest.kt similarity index 100% rename from core/interfaces/src/test/kotlin/app/aaps/core/interfaces/utils/SafeParseTest.kt rename to core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/utils/SafeParseTest.kt diff --git a/core/interfaces/src/main/AndroidManifest.xml b/core/interfaces/src/androidMain/AndroidManifest.xml similarity index 100% rename from core/interfaces/src/main/AndroidManifest.xml rename to core/interfaces/src/androidMain/AndroidManifest.xml diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APS.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APS.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APS.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APS.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/APSResult.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Loop.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/RT.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/RT.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/RT.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/automation/Automation.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/automation/Automation.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/APS.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/APS.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/APS.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/APS.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/AllConfigs.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/AllConfigs.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/AllConfigs.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/AllConfigs.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/ApplicationScope.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/ApplicationScope.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/ApplicationScope.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/ApplicationScope.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/NotNSClient.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/NotNSClient.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/NotNSClient.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/NotNSClient.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/PumpDriver.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/PumpDriver.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/di/PumpDriver.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/di/PumpDriver.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/FileListProvider.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/FileListProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/FileListProvider.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/FileListProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/ImportExportPrefs.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/Maintenance.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/Maintenance.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/Maintenance.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/Maintenance.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/Prefs.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/Prefs.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/Prefs.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/Prefs.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsFile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsMetadataKey.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsMetadataKey.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsMetadataKey.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsMetadataKey.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationHolder.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/Profile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt new file mode 100644 index 000000000000..0cf9ed550bdc --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt @@ -0,0 +1,37 @@ +package app.aaps.core.interfaces.profile + +import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.data.model.ICfg +import app.aaps.core.data.model.data.Block +import app.aaps.core.data.model.data.TargetBlock + +/** + * Pure profile like it's entered by user. Contains only data. + * + * It used to carry the source JSON alongside the blocks. Nothing read it — every consumer worked + * from the blocks — while every construction paid to build it, so it is gone. Render on demand with + * `Profile.toPureNsJson` when JSON is actually needed. + */ +class PureProfile( + var basalBlocks: List, + var isfBlocks: List, + var icBlocks: List, + var targetBlocks: List, + var iCfg: ICfg? = null, + var glucoseUnit: GlucoseUnit, + /** + * Offset from UTC in milliseconds, **for the moment the profile was built**. + * + * This was a whole `java.util.TimeZone`, but the only thing ever read off it was + * `rawOffset` - the zone's *standard* offset, which ignores daylight saving by definition. So + * `Europe/Prague` gave +01:00 even in July. The single consumer turns the number back into a zone + * name for the Nightscout profile document by looking for a zone at that offset **now**, and in + * July no European zone is at +01:00, so the document named an unrelated zone instead. + * + * Holding the resolved offset makes that impossible: the value is DST aware at the point it is + * taken, matching `utcOffset` on every other record and [app.aaps.core.data.time.systemUtcOffsetAt]. + * Nothing is lost that was not already lost - the zone's identity went away the moment `rawOffset` + * reduced it to a number. + */ + var utcOffset: Long +) \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/protection/PasswordCheck.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BlePreCheck.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Pump.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/rfcomm/RfcommTransport.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/rfcomm/RfcommTransport.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/rfcomm/RfcommTransport.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/rfcomm/RfcommTransport.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Callback.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Callback.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Command.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/Command.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Command.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/AapsSchedulers.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/AapsSchedulers.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/AapsSchedulers.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/AapsSchedulers.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/Event.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/Event.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/smsCommunicator/Sms.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/smsCommunicator/SmsCommunicator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/storage/Storage.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/storage/Storage.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/storage/Storage.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/storage/Storage.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/MidnightTime.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/fabric/FabricPrivacy.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/fabric/FabricPrivacy.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/fabric/FabricPrivacy.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/fabric/FabricPrivacy.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt rename to core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt diff --git a/core/interfaces/src/main/res/values-ar-rSA/strings.xml b/core/interfaces/src/androidMain/res/values-ar-rSA/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-ar-rSA/strings.xml rename to core/interfaces/src/androidMain/res/values-ar-rSA/strings.xml diff --git a/core/interfaces/src/main/res/values-bg-rBG/strings.xml b/core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-bg-rBG/strings.xml rename to core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml diff --git a/core/interfaces/src/main/res/values-ca-rES/strings.xml b/core/interfaces/src/androidMain/res/values-ca-rES/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-ca-rES/strings.xml rename to core/interfaces/src/androidMain/res/values-ca-rES/strings.xml diff --git a/core/interfaces/src/main/res/values-cs-rCZ/strings.xml b/core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-cs-rCZ/strings.xml rename to core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml diff --git a/core/interfaces/src/main/res/values-da-rDK/strings.xml b/core/interfaces/src/androidMain/res/values-da-rDK/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-da-rDK/strings.xml rename to core/interfaces/src/androidMain/res/values-da-rDK/strings.xml diff --git a/core/interfaces/src/main/res/values-de-rDE/strings.xml b/core/interfaces/src/androidMain/res/values-de-rDE/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-de-rDE/strings.xml rename to core/interfaces/src/androidMain/res/values-de-rDE/strings.xml diff --git a/core/interfaces/src/main/res/values-el-rGR/strings.xml b/core/interfaces/src/androidMain/res/values-el-rGR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-el-rGR/strings.xml rename to core/interfaces/src/androidMain/res/values-el-rGR/strings.xml diff --git a/core/interfaces/src/main/res/values-es-rES/strings.xml b/core/interfaces/src/androidMain/res/values-es-rES/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-es-rES/strings.xml rename to core/interfaces/src/androidMain/res/values-es-rES/strings.xml diff --git a/core/interfaces/src/main/res/values-fr-rFR/strings.xml b/core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-fr-rFR/strings.xml rename to core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml diff --git a/core/interfaces/src/main/res/values-hr-rHR/strings.xml b/core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-hr-rHR/strings.xml rename to core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml diff --git a/core/interfaces/src/main/res/values-hu-rHU/strings.xml b/core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-hu-rHU/strings.xml rename to core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml diff --git a/core/interfaces/src/main/res/values-it-rIT/strings.xml b/core/interfaces/src/androidMain/res/values-it-rIT/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-it-rIT/strings.xml rename to core/interfaces/src/androidMain/res/values-it-rIT/strings.xml diff --git a/core/interfaces/src/main/res/values-iw-rIL/strings.xml b/core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-iw-rIL/strings.xml rename to core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml diff --git a/core/interfaces/src/main/res/values-ko-rKR/strings.xml b/core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-ko-rKR/strings.xml rename to core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml diff --git a/core/interfaces/src/main/res/values-lt-rLT/strings.xml b/core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-lt-rLT/strings.xml rename to core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml diff --git a/core/interfaces/src/main/res/values-nb-rNO/strings.xml b/core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-nb-rNO/strings.xml rename to core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml diff --git a/core/interfaces/src/main/res/values-nl-rNL/strings.xml b/core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-nl-rNL/strings.xml rename to core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml diff --git a/core/interfaces/src/main/res/values-pl-rPL/strings.xml b/core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-pl-rPL/strings.xml rename to core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml diff --git a/core/interfaces/src/main/res/values-pt-rBR/strings.xml b/core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-pt-rBR/strings.xml rename to core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml diff --git a/core/interfaces/src/main/res/values-pt-rPT/strings.xml b/core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-pt-rPT/strings.xml rename to core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml diff --git a/core/interfaces/src/main/res/values-ro-rRO/strings.xml b/core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-ro-rRO/strings.xml rename to core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml diff --git a/core/interfaces/src/main/res/values-ru-rRU/strings.xml b/core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-ru-rRU/strings.xml rename to core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml diff --git a/core/interfaces/src/main/res/values-sk-rSK/strings.xml b/core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-sk-rSK/strings.xml rename to core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml diff --git a/core/interfaces/src/main/res/values-sr-rCS/strings.xml b/core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-sr-rCS/strings.xml rename to core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml diff --git a/core/interfaces/src/main/res/values-sv-rSE/strings.xml b/core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-sv-rSE/strings.xml rename to core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml diff --git a/core/interfaces/src/main/res/values-tr-rTR/strings.xml b/core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-tr-rTR/strings.xml rename to core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml diff --git a/core/interfaces/src/main/res/values-uk-rUA/strings.xml b/core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-uk-rUA/strings.xml rename to core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml diff --git a/core/interfaces/src/main/res/values-vi-rVN/strings.xml b/core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-vi-rVN/strings.xml rename to core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml diff --git a/core/interfaces/src/main/res/values-zh-rCN/strings.xml b/core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-zh-rCN/strings.xml rename to core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml diff --git a/core/interfaces/src/main/res/values-zh-rTW/strings.xml b/core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values-zh-rTW/strings.xml rename to core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml diff --git a/core/interfaces/src/main/res/values/strings.xml b/core/interfaces/src/androidMain/res/values/strings.xml similarity index 100% rename from core/interfaces/src/main/res/values/strings.xml rename to core/interfaces/src/androidMain/res/values/strings.xml diff --git a/core/interfaces/src/main/res/values/wear_paths.xml b/core/interfaces/src/androidMain/res/values/wear_paths.xml similarity index 100% rename from core/interfaces/src/main/res/values/wear_paths.xml rename to core/interfaces/src/androidMain/res/values/wear_paths.xml diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/annotations/OpenForTesting.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/annotations/OpenForTesting.kt new file mode 100644 index 000000000000..9aae295b2a95 --- /dev/null +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/annotations/OpenForTesting.kt @@ -0,0 +1,17 @@ +package app.aaps.annotations + +/** + * Annotate a class with [OpenForTesting] if it should be extendable for testing. + * + * There used to be two declarations of this, one per build variant, the debug one carrying an extra + * `@OpenClass` meta-annotation. Nothing ever read it: the allOpen plugin is configured with this + * annotation's own name (`all-open-dependencies.gradle.kts`), not with the meta-annotation, so the two + * declarations behaved identically and the variant split did nothing. One declaration now, and it + * holds no Android types, so it lives in common code. + * + * Note this means an annotated class is open in release builds too. That was already the case - the + * three modules applying allOpen apply it to every variant - it was just not visible while there were + * two files suggesting otherwise. + */ +@Target(AnnotationTarget.CLASS) +annotation class OpenForTesting \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/alerts/LocalAlertUtils.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/alerts/LocalAlertUtils.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/alerts/LocalAlertUtils.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/alerts/LocalAlertUtils.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/AutosensResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/CurrentTemp.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/CurrentTemp.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/CurrentTemp.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/CurrentTemp.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatus.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatus.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusAutoIsf.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusAutoIsf.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusAutoIsf.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusAutoIsf.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusSMB.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusSMB.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusSMB.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/GlucoseStatusSMB.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/IobTotal.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/IobTotal.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/IobTotal.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/IobTotal.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/MealData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/MealData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/MealData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/MealData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/OapsProfile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/OapsProfile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/OapsProfile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/OapsProfile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/OapsProfileAutoIsf.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/OapsProfileAutoIsf.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/OapsProfileAutoIsf.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/OapsProfileAutoIsf.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Predictions.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Predictions.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/aps/Predictions.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Predictions.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/autotune/Autotune.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/autotune/Autotune.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/autotune/Autotune.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/autotune/Autotune.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bgQualityCheck/BgQualityCheck.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bgQualityCheck/BgQualityCheck.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bgQualityCheck/BgQualityCheck.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bgQualityCheck/BgQualityCheck.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/BatchAction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/BatchAction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/bolus/BatchAction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/BatchAction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/AddEntryResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/AddEntryResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/AddEntryResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/AddEntryResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/Calibration.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/Calibration.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/Calibration.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/Calibration.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/CalibrationContext.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/CalibrationContext.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/calibration/CalibrationContext.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/calibration/CalibrationContext.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/Config.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/Config.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/Config.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/Config.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/RunningConfigurationKeys.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/RunningConfigurationKeys.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/configuration/RunningConfigurationKeys.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/RunningConfigurationKeys.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Constraint.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Constraint.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Constraint.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Constraint.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Objectives.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Objectives.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Objectives.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Objectives.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Safety.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Safety.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/constraints/Safety.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/Safety.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/ProcessedTbrEbData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/ProcessedTbrEbData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/db/ProcessedTbrEbData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/ProcessedTbrEbData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/dst/DstHelper.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/dst/DstHelper.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/dst/DstHelper.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/dst/DstHelper.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/graph/Scale.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/graph/Scale.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/graph/Scale.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/graph/Scale.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/graph/SeriesData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/graph/SeriesData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/graph/SeriesData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/graph/SeriesData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationType.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/iob/GlucoseStatusProvider.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/iob/GlucoseStatusProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/iob/GlucoseStatusProvider.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/iob/GlucoseStatusProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/local/LocaleDependentSetting.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/local/LocaleDependentSetting.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/local/LocaleDependentSetting.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/local/LocaleDependentSetting.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/AAPSLogger.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/AAPSLogger.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/AAPSLogger.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/AAPSLogger.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/L.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/L.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/L.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/L.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LTag.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LTag.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LogElement.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LogElement.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LogElement.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LogElement.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LoggerUtils.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LoggerUtils.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LoggerUtils.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/LoggerUtils.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/UserEntryLogger.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/UserEntryLogger.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/UserEntryLogger.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/logging/UserEntryLogger.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudModels.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudModels.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/CloudModels.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudModels.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/navigation/ElementCategory.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/navigation/ElementCategory.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/navigation/ElementCategory.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/navigation/ElementCategory.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/navigation/ElementType.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/navigation/ElementType.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/navigation/ElementType.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/navigation/ElementType.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AapsNotification.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmIntent.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSound.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationAction.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationCategory.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationCategory.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationCategory.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationCategory.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHandle.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationHandle.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationHandle.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationHandle.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationId.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationLevel.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationLevel.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/notifications/NotificationLevel.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationLevel.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSAlarm.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSAlarm.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSAlarm.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSAlarm.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSClientLog.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/NSClientRepository.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/StoreDataForDb.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/StoreDataForDb.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/nsclient/StoreDataForDb.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/StoreDataForDb.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/LastBgData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/LastBgData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/LastBgData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/LastBgData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/OverviewData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/OverviewData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/OverviewData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/OverviewData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/AapsClientStatusData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/AapsClientStatusData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/AapsClientStatusData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/AapsClientStatusData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/CalculationResults.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/CalculationResults.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/CalculationResults.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/CalculationResults.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/GraphConfiguration.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/GraphConfiguration.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/GraphConfiguration.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/GraphConfiguration.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/OverviewDataCache.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/OverviewDataCache.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/overview/graph/OverviewDataCache.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/overview/graph/OverviewDataCache.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/OwnDatabasePlugin.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/OwnDatabasePlugin.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/plugin/OwnDatabasePlugin.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/OwnDatabasePlugin.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profiling/Profiler.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profiling/Profiler.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profiling/Profiler.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profiling/Profiler.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/ExportPasswordDataStore.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/ProtectionCheck.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/SecureEncrypt.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/SecureEncrypt.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/protection/SecureEncrypt.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/protection/SecureEncrypt.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheckResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/BlePreCheckResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/BlePreCheckResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/BlePreCheckResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Dana.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Dana.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Dana.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Dana.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Diaconn.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Diaconn.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Diaconn.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Diaconn.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Insight.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Insight.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Insight.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Insight.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Medtrum.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Medtrum.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/Medtrum.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Medtrum.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/OmnipodDash.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/OmnipodDash.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/OmnipodDash.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/OmnipodDash.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/OmnipodEros.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/OmnipodEros.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/OmnipodEros.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/OmnipodEros.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpEnactResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpEnactResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpEnactResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpEnactResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpStatusProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/VirtualPump.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/VirtualPump.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/VirtualPump.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/VirtualPump.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/ble/BleTransport.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpDescriptionExtension.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDeviceState.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpDeviceState.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpDeviceState.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpDeviceState.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/defs/PumpTypeExtension.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/receivers/Intents.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/receivers/Intents.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/ReceiverStatusStore.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/receivers/ReceiverStatusStore.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/ReceiverStatusStore.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/receivers/ReceiverStatusStore.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfFile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfFile.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfFile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfFile.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/CwfMetaDataKey.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/LoopStatusData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/ResData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/ResData.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/ResData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/ResData.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/ResFormat.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/ResFormat.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/ResFormat.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/ResFormat.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/ActiveSceneSync.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/ActiveSceneSync.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/ActiveSceneSync.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/ActiveSceneSync.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/ClientControlSendResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/ClientControlSendResult.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/ClientControlSendResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/ClientControlSendResult.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneChainResolver.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneChainResolver.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneChainResolver.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneChainResolver.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneStore.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneStore.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/SceneStore.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneStore.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/Scenes.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/Scenes.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/scenes/Scenes.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/Scenes.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smoothing/Smoothing.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/smoothing/Smoothing.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/smoothing/Smoothing.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/smoothing/Smoothing.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/BgSource.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/BgSource.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/BgSource.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/BgSource.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/DexcomBoyda.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/DexcomBoyda.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/DexcomBoyda.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/DexcomBoyda.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/NSClientSource.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/NSClientSource.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/NSClientSource.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/NSClientSource.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/XDripSource.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/XDripSource.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/source/XDripSource.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/source/XDripSource.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/DexcomTIR.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTirCalculator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/DexcomTirCalculator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/DexcomTirCalculator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/DexcomTirCalculator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TIR.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TIR.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TIR.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TddCalculator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TddCalculator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TddCalculator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TddCalculator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TirCalculator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TirCalculator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/stats/TirCalculator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/stats/TirCalculator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/DataSyncSelector.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelectorXdrip.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/DataSyncSelectorXdrip.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/DataSyncSelectorXdrip.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/DataSyncSelectorXdrip.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/NsClient.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/NsClient.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/NsClient.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/NsClient.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/Sync.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/Sync.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/Sync.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/Sync.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/Tidepool.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/Tidepool.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/Tidepool.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/Tidepool.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/XDripBroadcast.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/XDripBroadcast.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/sync/XDripBroadcast.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/sync/XDripBroadcast.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/tempTargets/TempTargetPresetExtensions.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/IconsProvider.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/ui/IconsProvider.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/IconsProvider.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/ui/IconsProvider.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/HardLimits.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/HardLimits.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/HardLimits.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/HardLimits.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/Round.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Round.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/Round.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/SafeParse.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/SafeParse.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/SafeParse.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/SafeParse.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/TimeDiff.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Translator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/Translator.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/utils/Translator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/Translator.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionCheckerUtils.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/versionChecker/VersionCheckerUtils.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionCheckerUtils.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/versionChecker/VersionCheckerUtils.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/versionChecker/VersionDefinition.kt diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/widget/WidgetUpdater.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/widget/WidgetUpdater.kt similarity index 100% rename from core/interfaces/src/main/kotlin/app/aaps/core/interfaces/widget/WidgetUpdater.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/widget/WidgetUpdater.kt diff --git a/core/interfaces/src/debug/kotlin/app/aaps/annotations/OpenForTesting.kt b/core/interfaces/src/debug/kotlin/app/aaps/annotations/OpenForTesting.kt deleted file mode 100644 index 7e97b21b6e69..000000000000 --- a/core/interfaces/src/debug/kotlin/app/aaps/annotations/OpenForTesting.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.aaps.annotations - -/** - * This is the actual annotation that makes the class open. Don't use it directly, only through [OpenForTesting] - * which has a NOOP replacement in production. - */ -@Target(AnnotationTarget.ANNOTATION_CLASS) -annotation class OpenClass - -/** - * Annotate a class with [OpenForTesting] if it should be extendable for testing. - */ -@OpenClass -@Target(AnnotationTarget.CLASS) -annotation class OpenForTesting \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt deleted file mode 100644 index 198c6e057da7..000000000000 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.aaps.core.interfaces.profile - -import app.aaps.core.data.model.GlucoseUnit -import app.aaps.core.data.model.ICfg -import app.aaps.core.data.model.data.Block -import app.aaps.core.data.model.data.TargetBlock -import java.util.TimeZone - -/** - * Pure profile like it's entered by user. Contains only data. - * - * It used to carry the source JSON alongside the blocks. Nothing read it — every consumer worked - * from the blocks — while every construction paid to build it, so it is gone. Render on demand with - * `Profile.toPureNsJson` when JSON is actually needed. - */ -class PureProfile( - var basalBlocks: List, - var isfBlocks: List, - var icBlocks: List, - var targetBlocks: List, - var iCfg: ICfg? = null, - var glucoseUnit: GlucoseUnit, - var timeZone: TimeZone -) \ No newline at end of file diff --git a/core/interfaces/src/release/kotlin/app/aaps/annotations/OpenForTesting.kt b/core/interfaces/src/release/kotlin/app/aaps/annotations/OpenForTesting.kt deleted file mode 100644 index 4783a1db974f..000000000000 --- a/core/interfaces/src/release/kotlin/app/aaps/annotations/OpenForTesting.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.aaps.annotations - -/** - * Annotate a class with [OpenForTesting] if it should be extendable for testing. - * In production the class remains final. - */ -@Target(AnnotationTarget.CLASS) -annotation class OpenForTesting \ No newline at end of file diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt index 5241577c1882..a68d0be4bb0a 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ProfileSwitchExtension.kt @@ -4,6 +4,7 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.ICfg import app.aaps.core.data.model.PS +import app.aaps.core.data.time.systemUtcOffsetAt import app.aaps.core.data.time.T import app.aaps.core.interfaces.profile.PureProfile import app.aaps.core.interfaces.profile.SingleProfile @@ -11,8 +12,10 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.utils.JsonHelper +import kotlinx.datetime.TimeZone +import kotlinx.datetime.offsetAt import org.json.JSONObject -import java.util.TimeZone +import kotlin.time.Instant fun PS.getCustomizedName(decimalFormatter: DecimalFormatter): String { var name: String = profileName @@ -44,7 +47,7 @@ fun SingleProfile.toPureProfile(dateUtil: DateUtil): PureProfile? = icBlocks = ic, targetBlocks = target, glucoseUnit = if (mgdl) GlucoseUnit.MGDL else GlucoseUnit.MMOL, - timeZone = TimeZone.getDefault() + utcOffset = systemUtcOffsetAt(dateUtil.now()) ) /** @@ -57,7 +60,13 @@ fun pureProfileFromJson(jsonObject: JSONObject, dateUtil: DateUtil, defaultUnits val iCfg = JsonHelper.safeGetJSONObject(jsonObject, "iCfg", null)?.let { ICfg.fromJson(it) } - val timezone = TimeZone.getTimeZone(JsonHelper.safeGetString(jsonObject, "timezone", "UTC")) + // The offset AT THIS MOMENT, not the zone's standard offset. Taking `rawOffset` here is what + // made a summer Prague profile claim +01:00 and then get named after some unrelated zone that + // really is at +01:00 in July. `java.util.TimeZone.getTimeZone` quietly answered GMT for an id + // it did not know, and kotlinx throws instead, so that fallback is kept explicitly. + val zoneName = JsonHelper.safeGetString(jsonObject, "timezone", "UTC") + val zone = runCatching { TimeZone.of(zoneName) }.getOrDefault(TimeZone.UTC) + val utcOffset = zone.offsetAt(Instant.fromEpochMilliseconds(dateUtil.now())).totalSeconds * 1000L val isfBlocks = blockFromJsonArray(jsonObject.getJSONArray("sens"), dateUtil) ?: return null val icBlocks = blockFromJsonArray(jsonObject.getJSONArray("carbratio"), dateUtil) @@ -73,7 +82,7 @@ fun pureProfileFromJson(jsonObject: JSONObject, dateUtil: DateUtil, defaultUnits icBlocks = icBlocks, targetBlocks = targetBlocks, glucoseUnit = units, - timeZone = timezone, + utcOffset = utcOffset, iCfg = iCfg ) } catch (_: Exception) { diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index a56eb4d6055b..b39beae06162 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -1,6 +1,7 @@ package app.aaps.core.objects.profile import app.aaps.core.data.configuration.Constants +import app.aaps.core.data.time.systemUtcOffsetAt import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.ICfg @@ -40,7 +41,7 @@ import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.json.JSONArray import org.json.JSONObject -import java.util.TimeZone +import kotlin.time.Clock sealed class ProfileSealed( val id: Long, @@ -127,7 +128,7 @@ sealed class ProfileSealed( null, 0, 100, - value.timeZone.rawOffset.toLong(), + value.utcOffset, activePlugin?.activeAPS ) { @@ -154,7 +155,7 @@ sealed class ProfileSealed( null, 0, 100, - value.timeZone.rawOffset.toLong(), + value.utcOffset, null ), PumpProfile { @@ -384,7 +385,7 @@ sealed class ProfileSealed( targetBlocks = targetBlocks.shiftTargetBlock(timeshift), glucoseUnit = units, iCfg = iCfg, - timeZone = TimeZone.getDefault() + utcOffset = systemUtcOffsetAt(dateUtil.now()) ) /** @@ -492,7 +493,7 @@ sealed class ProfileSealed( icBlocks = icBlocks.shiftBlock(100.0 / percentage * iCfg.concentration, timeshift), targetBlocks = targetBlocks.shiftTargetBlock(timeshift), glucoseUnit = units, - timeZone = TimeZone.getDefault() + utcOffset = systemUtcOffsetAt(Clock.System.now().toEpochMilliseconds()) ), null ) diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileTimeZoneTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileTimeZoneTest.kt new file mode 100644 index 000000000000..7aa3052ddd23 --- /dev/null +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileTimeZoneTest.kt @@ -0,0 +1,109 @@ +package app.aaps.core.objects.profile + +import android.content.Context +import app.aaps.core.objects.extensions.pureProfileFromJson +import app.aaps.shared.impl.utils.DateUtilImpl +import app.aaps.shared.tests.TestBase +import com.google.common.truth.Truth.assertThat +import org.json.JSONObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +/** + * The time zone a profile carries, which had no coverage at all. + * + * It used to be a whole `java.util.TimeZone` and the only thing read off it was `rawOffset` - the + * zone's **standard** offset, which ignores daylight saving by definition. `Europe/Prague` therefore + * reported +01:00 in July as well as in January. + * + * That mattered because the single consumer, `toPureNsJson`, turns the number back into a zone + * *name* for the Nightscout profile document by searching for a zone sitting at that offset **now**. + * In July nothing in Europe is at +01:00, so the document ended up naming an unrelated zone. The + * value is now resolved for the moment the profile is read, so the two halves agree. + * + * `now()` is stubbed rather than taken from the clock, or these tests would only say anything during + * one half of the year. + */ +class ProfileTimeZoneTest : TestBase() { + + @Mock lateinit var context: Context + + private lateinit var dateUtil: DateUtilImpl + + // 2026-01-15T12:00Z and 2026-07-15T12:00Z: Prague is CET (+01:00) at the first and CEST (+02:00) + // at the second. + private val winter = 1_768_478_400_000L + private val summer = 1_784_116_800_000L + + private val oneHour = 3_600_000L + private val twoHours = 7_200_000L + + @BeforeEach fun prepare() { + dateUtil = spy(DateUtilImpl(context)) + } + + private fun profileIn(zone: String, at: Long) = run { + whenever(dateUtil.now()).thenReturn(at) + pureProfileFromJson(JSONObject(profileJson(zone)), dateUtil)!! + } + + @Test + fun `a zone on daylight saving reports the summer offset in summer`() { + assertThat(profileIn("Europe/Prague", summer).utcOffset).isEqualTo(twoHours) + } + + /** + * The regression this replaced: `rawOffset` gave one hour here *and* in summer, so the two were + * indistinguishable and summer was the wrong one. + */ + @Test + fun `the same zone reports the winter offset in winter`() { + assertThat(profileIn("Europe/Prague", winter).utcOffset).isEqualTo(oneHour) + } + + @Test + fun `a zone without daylight saving reads the same all year`() { + // Africa/Lagos is +01:00 the whole year round, which is exactly why it used to be picked as + // the name for a summer Prague profile. + assertThat(profileIn("Africa/Lagos", summer).utcOffset).isEqualTo(oneHour) + assertThat(profileIn("Africa/Lagos", winter).utcOffset).isEqualTo(oneHour) + } + + @Test + fun `UTC is zero in both halves of the year`() { + assertThat(profileIn("UTC", summer).utcOffset).isEqualTo(0L) + assertThat(profileIn("UTC", winter).utcOffset).isEqualTo(0L) + } + + /** + * `java.util.TimeZone.getTimeZone` answered GMT for an id it did not recognise, so a corrupt or + * unknown zone silently became UTC rather than failing the whole profile. kotlinx throws instead, + * so that fallback is explicit now - and this says it still holds. + */ + @Test + fun `an unknown zone falls back to UTC instead of failing the profile`() { + val profile = profileIn("Not/AZone", summer) + + assertThat(profile.utcOffset).isEqualTo(0L) + assertThat(profile.basalBlocks).isNotEmpty() // the rest of the profile still parsed + } + + /** A profile with no timezone at all also has to keep parsing. */ + @Test + fun `a missing zone falls back to UTC`() { + whenever(dateUtil.now()).thenReturn(summer) + val noZone = """{"dia":6,"carbratio":[{"time":"00:00","value":30}],"sens":[{"time":"00:00","value":3}],""" + + """"basal":[{"time":"00:00","value":1}],"target_low":[{"time":"00:00","value":4.5}],""" + + """"target_high":[{"time":"00:00","value":7}],"units":"mmol"}""" + + assertThat(pureProfileFromJson(JSONObject(noZone), dateUtil)!!.utcOffset).isEqualTo(0L) + } + + private fun profileJson(zone: String) = + """{"dia":6,"carbratio":[{"time":"00:00","value":30}],"sens":[{"time":"00:00","value":3}],""" + + """"basal":[{"time":"00:00","value":1}],"target_low":[{"time":"00:00","value":4.5}],""" + + """"target_high":[{"time":"00:00","value":7}],"units":"mmol","timezone":"$zone"}""" +} diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt index c4aa11083354..3e8fe70a0e91 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotuneCoreTest.kt @@ -21,7 +21,10 @@ import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.kotlin.whenever import java.io.File +import kotlinx.datetime.offsetAt import java.util.TimeZone +import kotlin.time.Instant +import kotlinx.datetime.TimeZone as KtTimeZone class AutotuneCoreTest : TestBaseWithProfile() { @@ -94,7 +97,7 @@ class AutotuneCoreTest : TestBaseWithProfile() { val dia = JsonHelper.safeGetDoubleAllowNull(jsonObject, "dia") ?: return null val peak = JsonHelper.safeGetIntAllowNull(jsonObject, "insulinPeakTime") ?: return null val iCfg = ICfg("insulin", peak, dia, 1.0) - val timezone = TimeZone.getTimeZone(JsonHelper.safeGetString(jsonObject, "timezone", "UTC")) + val zone = runCatching { KtTimeZone.of(JsonHelper.safeGetString(jsonObject, "timezone", "UTC")) }.getOrDefault(KtTimeZone.UTC) val isfJson = jsonObject.getJSONObject("isfProfile") val isfBlocks = ArrayList(1).also { val isfJsonArray = isfJson.getJSONArray("sensitivities") @@ -117,7 +120,7 @@ class AutotuneCoreTest : TestBaseWithProfile() { icBlocks = icBlocks, targetBlocks = targetBlocks, glucoseUnit = units, - timeZone = timezone + utcOffset = zone.offsetAt(Instant.fromEpochMilliseconds(dateUtil.now())).totalSeconds * 1000L ) return ATProfile(preferences, profileUtil, dateUtil, rh, profileStoreProvider, aapsLogger).with(ProfileSealed.Pure(pure, activePlugin), iCfg) } catch (_: Exception) { diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt index 9c29daf33f9f..f3f0e3bfe7f9 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/AutotunePrepTest.kt @@ -33,7 +33,10 @@ import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.kotlin.whenever import java.io.File +import kotlinx.datetime.offsetAt import java.util.TimeZone +import kotlin.time.Instant +import kotlinx.datetime.TimeZone as KtTimeZone class AutotunePrepTest : TestBaseWithProfile() { @@ -165,7 +168,7 @@ class AutotunePrepTest : TestBaseWithProfile() { val dia = JsonHelper.safeGetDoubleAllowNull(jsonObject, "dia") ?: return null val peak = JsonHelper.safeGetIntAllowNull(jsonObject, "insulinPeakTime") ?: return null val iCfg = ICfg("insulin", peak, dia, 1.0) - val timezone = TimeZone.getTimeZone(JsonHelper.safeGetString(jsonObject, "timezone", "UTC")) + val zone = runCatching { KtTimeZone.of(JsonHelper.safeGetString(jsonObject, "timezone", "UTC")) }.getOrDefault(KtTimeZone.UTC) val isfJson = jsonObject.getJSONObject("isfProfile") val isfBlocks = ArrayList(1).also { val isfJsonArray = isfJson.getJSONArray("sensitivities") @@ -188,7 +191,7 @@ class AutotunePrepTest : TestBaseWithProfile() { icBlocks = icBlocks, targetBlocks = targetBlocks, glucoseUnit = units, - timeZone = timezone + utcOffset = zone.offsetAt(Instant.fromEpochMilliseconds(dateUtil.now())).totalSeconds * 1000L ) return ATProfile(preferences, profileUtil, dateUtil, rh, profileStoreProvider, aapsLogger).with(ProfileSealed.Pure(pure, activePlugin), iCfg) } catch (_: Exception) { From 240a0f0a353393a2a81924a697d040b320c6a741 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 21:09:26 +0200 Subject: [PATCH 078/146] Fix crash on start when a remote config fetch succeeds MainApp reads VersionCheckerUtils.definition through Kotlin reflection, so the cast on the result is unchecked and the compiler cannot see it. The property became a kotlinx JsonObject in 3128760fdd while the cast still said JSONObject, and the app died with a ClassCastException the moment Firebase actually returned a config: java.lang.ClassCastException: kotlinx.serialization.json.JsonObject cannot be cast to org.json.JSONObject at app.aaps.MainApp.setupRemoteConfig$lambda$0$0(MainApp.kt:977) Nothing caught it earlier because the path needs network and Play Services to reach: it compiles, every unit test passes, and CI never gets there. Found by installing on an emulator. JsonHelper.merge was a shallow merge with the incoming keys winning, which is what plus on two maps does, so the merge itself stays a one liner. Verified on the emulator: the log line that used to be immediately followed by the crash - "RemoteConfig received successfully" - now appears with the process still alive and no fatal exception. --- app/src/main/kotlin/app/aaps/MainApp.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/MainApp.kt b/app/src/main/kotlin/app/aaps/MainApp.kt index fcf4f3a246b5..8923139922b9 100644 --- a/app/src/main/kotlin/app/aaps/MainApp.kt +++ b/app/src/main/kotlin/app/aaps/MainApp.kt @@ -77,7 +77,6 @@ import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.objects.crypto.CryptoUtil import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.locale.LocaleHelper -import app.aaps.core.utils.JsonHelper import app.aaps.database.AppRepository import app.aaps.implementation.lifecycle.ProcessLifecycleListener import app.aaps.implementation.plugin.PluginStore @@ -111,7 +110,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import rxdogtag2.RxDogTag import java.io.IOException import java.util.Locale @@ -974,8 +975,16 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { @Suppress("UNCHECKED_CAST") (versionCheckersUtils::class.declaredMemberProperties.find { it.name == "definition" } as KMutableProperty?) ?.let { - val merged = JsonHelper.merge(it.getter.call(versionCheckersUtils) as JSONObject, JSONObject(firebaseRemoteConfig.getString("defs"))) - it.setter.call(versionCheckersUtils, merged) + // `definition` is read through reflection, so the cast below is unchecked and the + // compiler cannot see it. It said JSONObject long after the property became a + // kotlinx JsonObject, and the app crashed on start as soon as a remote config + // fetch actually succeeded - which needs network and Play Services, so no test or + // CI build ever reached it. Keep the two types here in step by hand. + val current = it.getter.call(versionCheckersUtils) as JsonObject + val remote = Json.parseToJsonElement(firebaseRemoteConfig.getString("defs")).jsonObject + // Plus on the maps is a shallow merge with the remote keys winning, which is what + // the JsonHelper.merge that used to be here did. + it.setter.call(versionCheckersUtils, JsonObject(current + remote)) } } else aapsLogger.error("RemoteConfig fetch failed") } From 6197dd9349d643673393fccd081f8177b989796c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 21:30:21 +0200 Subject: [PATCH 079/146] Give :core:interfaces its Apple targets The flip shipped Android only, on the reasoning that the compose compiler plugin needs a Compose runtime on every target and the only one that resolves for Apple is CMP - which looked like a large adoption decision to fold into a source split. That was already answered. Wave 17 of _docs/KMP_IOS_FEASIBILITY.md records the in-repo CMP spike and its recipe: four plugins, not three, with org.jetbrains.compose alongside the compose compiler. Adding it plus cmp.runtime is all this needed. On Android CMP delegates to androidx, so the composeBom still decides the Android versions and nothing about the Android build changes. This matters beyond tidiness. The source split was made by compiling for iosArm64 and moving whatever failed, but without keeping the target that purity was a fact about one afternoon rather than something the build enforces. Now a java.* import added to commonMain fails instead of quietly compiling. No jvm() target, deliberately: it pulls in the desktop Compose surface (skiko-awt) and gives the module another way to fail without saying anything about iOS. Also from wave 17. Verified: iosArm64 and iosSimulatorArm64 compile, plus assembleFullDebug, testFullDebugUnitTest and testAndroidHostTest. --- core/interfaces/build.gradle.kts | 34 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index b7ddb405c198..8d32ff3e3df3 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -6,7 +6,12 @@ plugins { // "The 'com.android.library' (or 'com.android.application') plugin is not compatible with the // 'org.jetbrains.kotlin.multiplatform' plugin since AGP 9.0." alias(libs.plugins.android.kmp.library) + // The Compose COMPILER, which ships with Kotlin and compiles @Composable for every target. alias(libs.plugins.compose.compiler) + // The Compose Multiplatform framework. Needed because the compiler plugin above is applied per + // project rather than per target and fails any compilation with no Compose runtime on the class + // path - which is what kept the Apple targets out of this module at first. + alias(libs.plugins.compose.multiplatform) id("kotlinx-serialization") } @@ -50,23 +55,18 @@ kotlin { } } - // Only the Android target for now. Not because of the source split - see below. + // Apple klibs cross compile on Windows. Linking and running still need a Mac, and those tasks + // report SKIPPED rather than failing. // - // The compose compiler plugin is applied per project, not per target, and it fails ANY - // compilation that has no Compose runtime on the class path - a plain jvm() target as much as an - // Apple one. This module needs that plugin for exactly one declaration: - // `UserEntryPresentationHelper.iconColor` is `@Composable`. Every other Compose reference here is - // a plain type (ImageVector, Color, AnnotatedString) and needs only the dependency. + // These are what keep commonMain honest. The split was made by compiling for iosArm64 and moving + // whatever failed, so keeping the target means a java.* import added to commonMain later fails + // the build instead of quietly compiling on Android. // - // So the way to open the other targets is to move that one interface to :core:ui - both of its - // consumers, :implementation and :ui, already depend on it - and drop the plugin from here. That - // touches another module's API, so it is deliberately left as its own change rather than folded - // into the source split. - // - // The split itself was done against the Apple compiler: every file in commonMain was placed by - // compiling for iosArm64 and moving whatever failed, so commonMain is platform neutral as of this - // commit. What is missing without that target is the *enforcement* - a java.* import added to - // commonMain later would compile fine on Android and nothing would object. + // Deliberately no jvm() target: it pulls in the desktop Compose surface (skiko-awt) and gives the + // module another way to fail without saying anything about iOS. Recorded in wave 17 of + // _docs/KMP_IOS_FEASIBILITY.md. + iosArm64() + iosSimulatorArm64() sourceSets { commonMain { @@ -86,6 +86,10 @@ kotlin { // Multiplatform since 1.4.0, so LongSparseArray is usable from common code. // AutosensDataStore, TddCalculator and TirCalculator all expose it, so it stays api. api(libs.androidx.collection) + // The CMP runtime, so the compose compiler plugin has something to compile against on + // every target. On Android CMP delegates to androidx, so the composeBom still decides + // the Android versions and nothing about the Android build changes. + implementation(libs.cmp.runtime) } } From 96e08ee7e83bfaeede6f5000c50d4b210cc139e4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 22:08:09 +0200 Subject: [PATCH 080/146] Pull the icon-only interfaces back into commonMain The split put 138 files in androidMain, but only 34 of them had a platform import at all - the other 104 were there because they referenced one that did. Several of the roots were not Android for any real reason: they carried an ImageVector, and that alone was enough. ImageVector lives in cmp.ui, not cmp.runtime, so adding that dependency is what actually unblocks them. Six interfaces move back: CustomAction and its CustomActionType, SceneIconResolver, AutomationEvent, CloudDirectoryManager, CloudStorageProvider, PrefsStatus and UserEntryPresentationHelper. UserEntryPresentationHelper is the interesting one. Its @Composable iconColor was the single declaration that kept the compose compiler plugin here and, by extension, kept the Apple targets out until wave 17's recipe was applied. It is now common code. Left in androidMain deliberately: PluginDescription needs java.lang.Class, CommandQueue reaches DetailedBolusInfo and PumpSync, EventShowDialog needs Event, which uses commons-lang3 reflection for its toString. Those are real entanglements rather than an icon field. 125 common / 130 android now, from 117 / 138. Verified: iosArm64 and iosSimulatorArm64 compile, plus assembleFullDebug, testFullDebugUnitTest and testAndroidHostTest. --- core/interfaces/build.gradle.kts | 5 ++++- .../app/aaps/core/interfaces/automation/AutomationEvent.kt | 0 .../core/interfaces/maintenance/CloudDirectoryManager.kt | 0 .../aaps/core/interfaces/maintenance/CloudStorageProvider.kt | 0 .../app/aaps/core/interfaces/maintenance/PrefsStatus.kt | 0 .../app/aaps/core/interfaces/pump/actions/CustomAction.kt | 0 .../aaps/core/interfaces/pump/actions/CustomActionType.kt | 0 .../app/aaps/core/interfaces/scenes/SceneIconResolver.kt | 0 .../core/interfaces/userEntry/UserEntryPresentationHelper.kt | 0 9 files changed, 4 insertions(+), 1 deletion(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt (100%) diff --git a/core/interfaces/build.gradle.kts b/core/interfaces/build.gradle.kts index 8d32ff3e3df3..b298658ca78f 100644 --- a/core/interfaces/build.gradle.kts +++ b/core/interfaces/build.gradle.kts @@ -89,7 +89,10 @@ kotlin { // The CMP runtime, so the compose compiler plugin has something to compile against on // every target. On Android CMP delegates to androidx, so the composeBom still decides // the Android versions and nothing about the Android build changes. - implementation(libs.cmp.runtime) + api(libs.cmp.runtime) + // ImageVector and friends live here, not in the runtime. Several interfaces in this + // module carry an icon, and that alone used to be enough to make them Android only. + api(libs.cmp.ui) } } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/automation/AutomationEvent.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudDirectoryManager.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/CloudStorageProvider.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/PrefsStatus.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomAction.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/actions/CustomActionType.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneIconResolver.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/userEntry/UserEntryPresentationHelper.kt From f14a70d9e27cc886bfd0ae98a1e309b35df01206 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 14 Aug 2026 22:40:30 +0200 Subject: [PATCH 081/146] DetailedBolusInfo to commonMain, missingPermissions to an extension DetailedBolusInfo had no Android or JVM import at all. Three @JvmField annotations kept it out of common code - kotlin.jvm does not exist on Kotlin/Native - and because it sits in the middle of the bolus path it took CommandQueue and others with it. @JvmField was not decorative. It exposes the property as a real Java field, and AapsOmnipodErosManager.java read and wrote insulin, carbs and timestamp directly in twelve places. Those are accessors now. Only that one file needed it; the rest of the 140 Java files never touched these fields. It also broke a medtrum test in a way worth recording. It wrote the timestamp onto a Mockito mock and said so: detailedBolusInfo.timestamp = timestamp // Wierd way to mock but this is a @JvmField That worked only because a field write bypasses the mock. As an ordinary property it goes to a stubbed setter and is dropped. The test builds a real DetailedBolusInfo now, which is what it wanted anyway - the class is plain data with nothing to mock. missingPermissions(context) moves off PluginBase and becomes an extension in androidMain. It asked Android whether a permission is granted and was the only member of that class doing anything Android. PluginBase still cannot move - it holds a ResourceHelper for name, nameShort and description, and that is the next thing to look at - but its Android surface is now one dependency rather than three imports plus a method. 126 common / 130 android. Verified: assembleFullDebug, testFullDebugUnitTest, testAndroidHostTest, iosArm64 and iosSimulatorArm64. --- .../aaps/core/interfaces/plugin/PluginBase.kt | 14 ----------- .../interfaces/plugin/PluginBaseExtension.kt | 21 ++++++++++++++++ .../core/interfaces/pump/DetailedBolusInfo.kt | 6 ++--- .../aaps/implementation/plugin/PluginStore.kt | 1 + .../comm/packets/GetRecordPacketTest.kt | 12 ++++++---- .../eros/manager/AapsOmnipodErosManager.java | 24 +++++++++---------- 6 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseExtension.kt rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt (95%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt index fca8ec43ba0a..bfd9d7b4438c 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt @@ -1,8 +1,5 @@ package app.aaps.core.interfaces.plugin -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag @@ -174,15 +171,4 @@ abstract class PluginBase( * Override in subclasses to declare permissions. */ open fun requiredPermissions(): List = emptyList() - - /** - * Returns [requiredPermissions] that are not yet granted. - * Special permission groups are excluded — they need dedicated checks. - */ - fun missingPermissions(context: Context): List = - requiredPermissions().filter { group -> - !group.special && group.permissions.any { permission -> - ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED - } - } } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseExtension.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseExtension.kt new file mode 100644 index 000000000000..4bd8116bdbca --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseExtension.kt @@ -0,0 +1,21 @@ +package app.aaps.core.interfaces.plugin + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +/** + * Returns [PluginBase.requiredPermissions] that are not yet granted. + * Special permission groups are excluded - they need dedicated checks. + * + * This was a member of [PluginBase]. It took a [Context] and asked Android whether a permission is + * granted, and it was the only thing in that class that touched Android at all - so one method kept + * the base class every plugin extends, and everything that names it, out of common code. As an + * extension it lives where it belongs and [PluginBase] itself is platform neutral. + */ +fun PluginBase.missingPermissions(context: Context): List = + requiredPermissions().filter { group -> + !group.special && group.permissions.any { permission -> + ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED + } + } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt similarity index 95% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt index d54c796b2ce9..e2d5b6e68ecc 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfo.kt @@ -14,11 +14,11 @@ class DetailedBolusInfo { val id = Clock.System.now().toEpochMilliseconds() // Requesting parameters for driver - @JvmField var insulin = 0.0 - @JvmField var carbs = 0.0 + var insulin = 0.0 + var carbs = 0.0 // Additional requesting parameters - @JvmField var timestamp = Clock.System.now().toEpochMilliseconds() + var timestamp = Clock.System.now().toEpochMilliseconds() var lastKnownBolusTime: Long = 0 // for SMB check var deliverAtTheLatest: Long = 0 // SMB should be delivered within 1 min from this time diff --git a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt index 0634bc85a1c0..44d1a56aa451 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt @@ -24,6 +24,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PermissionGroup import app.aaps.core.interfaces.plugin.PermissionProvider import app.aaps.core.interfaces.plugin.PluginBase +import app.aaps.core.interfaces.plugin.missingPermissions import app.aaps.core.interfaces.plugin.PluginBaseWithPreferences import app.aaps.core.interfaces.pump.Pump import app.aaps.core.interfaces.pump.PumpWithConcentration diff --git a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/comm/packets/GetRecordPacketTest.kt b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/comm/packets/GetRecordPacketTest.kt index c8b346b37cb0..966514b3072b 100644 --- a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/comm/packets/GetRecordPacketTest.kt +++ b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/comm/packets/GetRecordPacketTest.kt @@ -88,10 +88,14 @@ class GetRecordPacketTest : MedtrumTestBase() { val bolusType = BS.Type.SMB val amount = 1.1 - // Mocks - val detailedBolusInfo: DetailedBolusInfo = mock() - detailedBolusInfo.timestamp = timestamp // Wierd way to mock but this is a @JvmField - whenever(detailedBolusInfo.bolusType).thenReturn(bolusType) + // A real instance, not a mock. The timestamp used to be written straight onto a mock's field, + // which worked only because it was a @JvmField and so bypassed Mockito. It is an ordinary + // property now, so that write would go to a stubbed setter and be dropped. DetailedBolusInfo + // is plain data, so there is nothing to mock here anyway. + val detailedBolusInfo = DetailedBolusInfo().apply { + this.timestamp = timestamp + this.bolusType = bolusType + } whenever(detailedBolusInfoStorage.findDetailedBolusInfo(timestamp, amount)).thenReturn(detailedBolusInfo) diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java index 35cd5b82e78f..c848bb6aec4c 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/manager/AapsOmnipodErosManager.java @@ -397,7 +397,7 @@ public PumpEnactResult bolus(DetailedBolusInfo detailedBolusInfo) { Date bolusStarted; try { - bolusCommandResult = executeCommand(() -> delegate.bolus(PumpTypeExtensionKt.determineCorrectBolusSize(PumpType.OMNIPOD_EROS, detailedBolusInfo.insulin), beepsEnabled, beepsEnabled, + bolusCommandResult = executeCommand(() -> delegate.bolus(PumpTypeExtensionKt.determineCorrectBolusSize(PumpType.OMNIPOD_EROS, detailedBolusInfo.getInsulin()), beepsEnabled, beepsEnabled, detailedBolusInfo.getBolusType() == BS.Type.SMB ? null : (estimatedUnitsDelivered, percentage) -> { bolusProgressData.updateProgress(new PumpInsulin(estimatedUnitsDelivered)); @@ -413,13 +413,13 @@ public PumpEnactResult bolus(DetailedBolusInfo detailedBolusInfo) { if (OmnipodManager.CommandDeliveryStatus.UNCERTAIN_FAILURE.equals(bolusCommandResult.getCommandDeliveryStatus())) { // For safety reasons, we treat this as a bolus that has successfully been delivered, in order to prevent insulin overdose if (detailedBolusInfo.getBolusType() == BS.Type.SMB) { - showNotification(NotificationId.OMNIPOD_UNCERTAIN_SMB, getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain_smb, detailedBolusInfo.insulin), NotificationLevel.IMPORTANT, isNotificationUncertainSmbSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); + showNotification(NotificationId.OMNIPOD_UNCERTAIN_SMB, getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain_smb, detailedBolusInfo.getInsulin()), NotificationLevel.IMPORTANT, isNotificationUncertainSmbSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); } else { showErrorDialog(getStringResource(R.string.omnipod_eros_error_bolus_failed_uncertain), isNotificationUncertainBolusSoundEnabled() ? AlarmSound.BOLUS_ERROR : null); } } - detailedBolusInfo.timestamp = bolusStarted.getTime(); + detailedBolusInfo.setTimestamp(bolusStarted.getTime()); detailedBolusInfo.setPumpType(PumpType.OMNIPOD_EROS); detailedBolusInfo.setPumpSerial(serialNumber()); @@ -456,13 +456,13 @@ public PumpEnactResult bolus(DetailedBolusInfo detailedBolusInfo) { OmnipodManager.BolusDeliveryResult bolusDeliveryResult = bolusCommandResult.getDeliveryResultSubject().blockingGet(); - detailedBolusInfo.insulin = bolusDeliveryResult.getUnitsDelivered(); + detailedBolusInfo.setInsulin(bolusDeliveryResult.getUnitsDelivered()); addBolusToHistory(detailedBolusInfo); preferences.remove(ErosStringNonPreferenceKey.ActiveBolus); - return pumpEnactResultProvider.get().success(true).enacted(true).bolusDelivered(detailedBolusInfo.insulin); + return pumpEnactResultProvider.get().success(true).enacted(true).bolusDelivered(detailedBolusInfo.getInsulin()); } public PumpEnactResult cancelBolus() { @@ -726,17 +726,17 @@ public boolean isAutomaticallyAcknowledgeAlertsEnabled() { public void addBolusToHistory(DetailedBolusInfo originalDetailedBolusInfo) { DetailedBolusInfo detailedBolusInfo = originalDetailedBolusInfo.copy(); - detailedBolusInfo.setBolusTimestamp(detailedBolusInfo.timestamp); + detailedBolusInfo.setBolusTimestamp(detailedBolusInfo.getTimestamp()); detailedBolusInfo.setPumpType(PumpType.OMNIPOD_EROS); detailedBolusInfo.setPumpSerial(serialNumber()); - detailedBolusInfo.setBolusPumpId(addSuccessToHistory(detailedBolusInfo.timestamp, PodHistoryEntryType.SET_BOLUS, detailedBolusInfo.insulin + ";" + detailedBolusInfo.carbs)); + detailedBolusInfo.setBolusPumpId(addSuccessToHistory(detailedBolusInfo.getTimestamp(), PodHistoryEntryType.SET_BOLUS, detailedBolusInfo.getInsulin() + ";" + detailedBolusInfo.getCarbs())); - if (detailedBolusInfo.carbs > 0 && detailedBolusInfo.getCarbsTimestamp() != null) { + if (detailedBolusInfo.getCarbs() > 0 && detailedBolusInfo.getCarbsTimestamp() != null) { // split out a separate carbs record without a pumpId runSuspend( (scope, continuation) -> pumpSync.syncCarbsWithTimestamp( detailedBolusInfo.getCarbsTimestamp(), - detailedBolusInfo.carbs, + detailedBolusInfo.getCarbs(), null, PumpType.USER, serialNumber(), @@ -746,12 +746,12 @@ public void addBolusToHistory(DetailedBolusInfo originalDetailedBolusInfo) { // remove carbs from bolusInfo to not trigger any unwanted code paths in // TreatmentsPlugin.addToHistoryTreatment() method - detailedBolusInfo.carbs = 0; + detailedBolusInfo.setCarbs(0); } runSuspend( (scope, continuation) -> pumpSync.syncBolusWithPumpId( - detailedBolusInfo.timestamp, - new PumpInsulin(detailedBolusInfo.insulin), + detailedBolusInfo.getTimestamp(), + new PumpInsulin(detailedBolusInfo.getInsulin()), detailedBolusInfo.getBolusType(), detailedBolusInfo.getBolusPumpId(), detailedBolusInfo.getPumpType(), From bd09777ee7260df1ee722599dc5532474940fa5a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 09:56:07 +0200 Subject: [PATCH 082/146] XdripPlugin listens on Flow instead of Observable First slice of the listen-side migration. The bus already publishes to both a PublishSubject and a MutableSharedFlow, and send() has no Rx in its signature, so the 629 send sites need nothing - only the 107 remaining toObservable listeners do. This file already had the answer in it: a CoroutineScope on Dispatchers.IO and collectResilient, used for the persistence observer right next to the Rx code. collectResilient logs and continues per item, which is what subscribe(onNext, onError) did, and the IO scope is what observeOn(aapsSchedulers.io) gave these subscriptions - so the threading is preserved rather than left to the collector. Found while converting: onStop cleared the CompositeDisposable but never stopped anything on the scope, so the persistence observer kept running after the plugin stopped and a restart would stack a second collector on the first. onStop now cancels the scope's children. The scope itself stays alive because onStart can run again. CompositeDisposable and AapsSchedulers are gone from this plugin. Removing the constructor parameter shifted the positional arguments in XdripPluginTest, which is why that file changes too. 104 toObservable sites left. --- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 28 ++++++++----------- .../plugins/sync/xdrip/XdripPluginTest.kt | 1 - 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index b89ec48ade25..16c8ef709243 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -28,7 +28,6 @@ import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit @@ -59,11 +58,10 @@ import app.aaps.plugins.sync.xdrip.keys.XdripIntentKey import app.aaps.plugins.sync.xdrip.keys.XdripLongKey import app.aaps.plugins.sync.xdrip.workers.XdripDataSyncWorker import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -82,7 +80,6 @@ class XdripPlugin @Inject constructor( preferences: Preferences, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, - private val aapsSchedulers: AapsSchedulers, private val context: Context, private val fabricPrivacy: FabricPrivacy, private val loop: Loop, @@ -119,7 +116,6 @@ class XdripPlugin @Inject constructor( private val XDRIP_JOB_NAME: String = this::class.java.simpleName private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val disposable = CompositeDisposable() private var handler: Handler? = null // Not used Sync interface members @@ -130,10 +126,9 @@ class XdripPlugin @Inject constructor( override suspend fun onStart() { super.onStart() handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ WorkManager.getInstance(context).cancelUniqueWork(XDRIP_JOB_NAME) }, fabricPrivacy::logException) + // scope is Dispatchers.IO, which is what observeOn(aapsSchedulers.io) gave these before. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.XDRIP) { WorkManager.getInstance(context).cancelUniqueWork(XDRIP_JOB_NAME) } persistenceLayer.observeAnyChange() // HR/SC writes come from the watch; this plugin doesn't broadcast them — skip to avoid reconnect-flush storm. .filter { types -> types.any { it != HR::class && it != SC::class } } @@ -141,12 +136,10 @@ class XdripPlugin @Inject constructor( sendStatusLine() delayAndScheduleExecution("DB_CHANGED(${types.joinToString { it.simpleName ?: "?" }})") } - disposable += rxBus.toObservable(EventAutosensCalculationFinished::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ sendStatusLine() }, fabricPrivacy::logException) - disposable += rxBus.toObservable(EventAppInitialized::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ sendStatusLine() }, fabricPrivacy::logException) + rxBus.toFlow(EventAutosensCalculationFinished::class.java) + .collectResilient(scope, aapsLogger, LTag.XDRIP) { sendStatusLine() } + rxBus.toFlow(EventAppInitialized::class.java) + .collectResilient(scope, aapsLogger, LTag.XDRIP) { sendStatusLine() } eventWorker = Executors.newSingleThreadScheduledExecutor() } @@ -157,7 +150,10 @@ class XdripPlugin @Inject constructor( handler = null eventWorker?.shutdown() eventWorker = null - disposable.clear() + // Cancel the collectors, not the scope: onStart can run again and needs it alive. This was + // missing before - the DB observer below was already started on this scope and never stopped, + // so a restart stacked a second collector on top of the first. + scope.coroutineContext.cancelChildren() } private fun addLog(action: String, logText: String?) { diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/xdrip/XdripPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/xdrip/XdripPluginTest.kt index 1b097474b17f..479afec3a445 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/xdrip/XdripPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/xdrip/XdripPluginTest.kt @@ -29,7 +29,6 @@ class XdripPluginTest : TestBaseWithProfile() { preferences, profileFunction, profileUtil, - aapsSchedulers, context, fabricPrivacy, loop, From 333ac80fa7e38dc7b8ad78aa74561a69070c4475 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 11:38:23 +0200 Subject: [PATCH 083/146] TizenPlugin and TidepoolPlugin listen on Flow Same substitution as XdripPlugin: toFlow + collectResilient on the plugin's own IO scope, which is the scheduler observeOn(aapsSchedulers.io) was supplying. Both files already collected other sources this way, so the Rx blocks were the odd ones out. Neither had the teardown problem XdripPlugin did - both cancel their scope in onStop and rebuild it in onStart, so the collectors stop with the plugin. CompositeDisposable, AapsSchedulers and FabricPrivacy are all unused in these two now and gone. FabricPrivacy was only ever there as the Rx onError handler; collectResilient logs through AAPSLogger instead. Removing constructor parameters shifted the positional arguments in both tests, which is the rest of the diff. 100 toObservable sites left, from 107. --- .../plugins/sync/tidepool/TidepoolPlugin.kt | 31 ++++++------------- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 21 +++---------- .../sync/tidepool/TidepoolPluginTest.kt | 10 +++--- .../plugins/sync/tizen/TizenPluginTest.kt | 2 +- 4 files changed, 21 insertions(+), 43 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index a5dfb990e736..4ebd389fafee 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -9,14 +9,12 @@ import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.plugin.PluginBaseWithPreferences import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventSWSyncStatus import app.aaps.core.interfaces.sync.Sync import app.aaps.core.interfaces.sync.Tidepool import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences @@ -35,8 +33,6 @@ import app.aaps.plugins.sync.tidepool.keys.TidepoolBooleanKey import app.aaps.plugins.sync.tidepool.keys.TidepoolLongNonKey import app.aaps.plugins.sync.tidepool.keys.TidepoolStringNonKey import app.aaps.plugins.sync.tidepool.utils.RateLimit -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -51,9 +47,7 @@ class TidepoolPlugin @Inject constructor( aapsLogger: AAPSLogger, rh: ResourceHelper, preferences: Preferences, - private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, - private val fabricPrivacy: FabricPrivacy, private val tidepoolUploader: TidepoolUploader, private val uploadChunk: UploadChunk, private val rateLimit: RateLimit, @@ -87,7 +81,6 @@ class TidepoolPlugin @Inject constructor( aapsLogger, rh, preferences ) { - private var disposable: CompositeDisposable = CompositeDisposable() private var scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val isAllowed get() = receiverDelegate.allowed @@ -102,19 +95,16 @@ class TidepoolPlugin @Inject constructor( tidepoolUploader.resetInstance() if (isAllowed) doUpload("CONNECTIVITY") } - disposable += rxBus - .toObservable(EventTidepoolDoUpload::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ doUpload(EventTidepoolDoUpload::class.simpleName) }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventTidepoolStatus::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ event -> - tidepoolRepository.addLog(event.status) - tidepoolRepository.updateConnectionStatus(authFlowOut.connectionStatus) - // Pass to setup wizard - rxBus.send(EventSWSyncStatus(event.status)) - }, fabricPrivacy::logException) + // scope is Dispatchers.IO, matching the scheduler these subscriptions used before. + rxBus.toFlow(EventTidepoolDoUpload::class.java) + .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { doUpload(EventTidepoolDoUpload::class.simpleName) } + rxBus.toFlow(EventTidepoolStatus::class.java) + .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { event -> + tidepoolRepository.addLog(event.status) + tidepoolRepository.updateConnectionStatus(authFlowOut.connectionStatus) + // Pass to setup wizard + rxBus.send(EventSWSyncStatus(event.status)) + } persistenceLayer.observeChanges(GV::class.java) .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { gvList -> gvList.maxByOrNull { it.timestamp }?.let { gv -> @@ -133,7 +123,6 @@ class TidepoolPlugin @Inject constructor( override suspend fun onStop() { scope.cancel() - disposable.clear() super.onStop() } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 3ea317b3c356..8abcd575e79e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -23,14 +23,12 @@ import app.aaps.core.interfaces.pump.PumpStatusProvider import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.receivers.ReceiverStatusStore import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.Event import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.durationInMinutes @@ -39,8 +37,6 @@ import app.aaps.core.objects.extensions.toStringFull import app.aaps.core.ui.compose.icons.IcPluginTizen import app.aaps.plugins.sync.R import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -53,10 +49,8 @@ import javax.inject.Singleton class TizenPlugin @Inject constructor( aapsLogger: AAPSLogger, rh: ResourceHelper, - private val aapsSchedulers: AapsSchedulers, private val context: Context, private val dateUtil: DateUtil, - private val fabricPrivacy: FabricPrivacy, private val rxBus: RxBus, private val iobCobCalculator: IobCobCalculator, private val processedTbrEbData: ProcessedTbrEbData, @@ -80,21 +74,17 @@ class TizenPlugin @Inject constructor( aapsLogger, rh ) { - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null override suspend fun onStart() { super.onStart() val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - disposable += rxBus - .toObservable(EventLoopUpdateGui::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ sendData(it) }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventAutosensCalculationFinished::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ sendData(it) }, fabricPrivacy::logException) + // newScope is Dispatchers.IO, matching the scheduler these subscriptions used before. + rxBus.toFlow(EventLoopUpdateGui::class.java) + .collectResilient(newScope, aapsLogger, LTag.CORE) { sendData(it) } + rxBus.toFlow(EventAutosensCalculationFinished::class.java) + .collectResilient(newScope, aapsLogger, LTag.CORE) { sendData(it) } bolusProgressData.state .collectResilient(newScope, aapsLogger, LTag.CORE) { state -> if (state != null && !state.isSMB) { @@ -104,7 +94,6 @@ class TizenPlugin @Inject constructor( } override suspend fun onStop() { - disposable.clear() scope?.cancel() scope = null super.onStop() diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt index 8a0d6cdf8086..002eb9eb2ffb 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt @@ -47,7 +47,7 @@ class TidepoolPluginTest : TestBaseWithProfile() { whenever(receiverDelegate.connectivityStatusFlow).thenReturn(connectivityFlow) whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) tidepoolPlugin = TidepoolPlugin( - aapsLogger, rh, preferences, aapsSchedulers, rxBus, fabricPrivacy, tidepoolUploader, uploadChunk, rateLimit, receiverDelegate, authFlowOut, tidepoolRepository, dateUtil, persistenceLayer + aapsLogger, rh, preferences, rxBus, tidepoolUploader, uploadChunk, rateLimit, receiverDelegate, authFlowOut, tidepoolRepository, dateUtil, persistenceLayer ) } @@ -92,8 +92,8 @@ class TidepoolPluginTest : TestBaseWithProfile() { dateUtil, receiverDelegate, config, l, authFlowOut, rateLimit ) val plugin = TidepoolPlugin( - aapsLogger, rh, preferences, aapsSchedulers, rxBus, - fabricPrivacy, realUploader, uploadChunk, rateLimit, + aapsLogger, rh, preferences, rxBus, + realUploader, uploadChunk, rateLimit, receiverDelegate, authFlowOut, tidepoolRepository, dateUtil, persistenceLayer ) runBlocking { plugin.onStart() } @@ -116,8 +116,8 @@ class TidepoolPluginTest : TestBaseWithProfile() { dateUtil, receiverDelegate, config, l, authFlowOut, rateLimit ) val plugin = TidepoolPlugin( - aapsLogger, rh, preferences, aapsSchedulers, rxBus, - fabricPrivacy, realUploader, uploadChunk, rateLimit, + aapsLogger, rh, preferences, rxBus, + realUploader, uploadChunk, rateLimit, receiverDelegate, authFlowOut, tidepoolRepository, dateUtil, persistenceLayer ) runBlocking { plugin.onStart() } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt index 27d3359553c0..31fe4e5d1d7f 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt @@ -47,7 +47,7 @@ internal class TizenPluginTest : TestBaseWithProfile() { @BeforeEach fun setUp() { sut = TizenPlugin( - aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, processedTbrEbData, profileFunction, preferences, processedDeviceStatusData, + aapsLogger, rh, context, dateUtil, rxBus, iobCobCalculator, processedTbrEbData, profileFunction, preferences, processedDeviceStatusData, loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, pumpStatusProvider, bolusProgressData ) // gs(TextRef) is a DEFAULT interface method, so a mock answers null unless it is stubbed. From 2fffeea8715c679e6c073bcb0000d0d3e1745f44 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 12:28:58 +0200 Subject: [PATCH 084/146] Close the subscription window when collecting the event bus collectResilient ends in launchIn, which only schedules the collector - it has not subscribed when the call returns. RxBus publishes through a MutableSharedFlow with replay 0, so anything emitted in that gap is dropped silently. RxJava's subscribe() registered synchronously and had no such window, so every subscription converted from Rx quietly acquires one. This is not theoretical. It is what made the wear DataHandlerMobile conversion fail: the test reported zero interactions, not a late one - the event was gone. That conversion is not in this commit; it needs this first. collectResilient takes a start parameter now, defaulting to the existing behaviour. The bus subscriptions in the three converted plugins pass UNDISPATCHED, so the coroutine runs on the calling thread as far as the subscribe and resumes on the scope's dispatcher after. The DB and StateFlow collectors in those same files keep the default deliberately: their sources replay, so a late subscriber loses nothing, and UNDISPATCHED would run a StateFlow's current value on whatever thread called onStart. Three tests pin it, including one asserting the default has NOT subscribed when the call returns - which is the property that makes this a trap rather than a style choice. --- .../rx/ResilientCollectStartTest.kt | 75 +++++++++++++++++++ .../core/interfaces/rx/ResilientCollect.kt | 24 +++++- .../plugins/sync/tidepool/TidepoolPlugin.kt | 5 +- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 5 +- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 7 +- 5 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ResilientCollectStartTest.kt diff --git a/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ResilientCollectStartTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ResilientCollectStartTest.kt new file mode 100644 index 000000000000..c81f5d21c09c --- /dev/null +++ b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ResilientCollectStartTest.kt @@ -0,0 +1,75 @@ +package app.aaps.core.interfaces.rx + +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock + +/** + * The subscription window in [collectResilient], and the [CoroutineStart] that closes it. + * + * This is the shape `RxBus` publishes through - a `MutableSharedFlow` with **replay 0** - so anything + * emitted before a collector has actually subscribed is dropped, with no error and no log. The default + * `launchIn` only *schedules* the collector, so converting an RxJava `subscribe()` (which registered + * synchronously) to a Flow collector silently acquires that window. + * + * The first test is the interesting one: it does not assert the event is lost, because that is a race + * and would be flaky in the other direction. It asserts the guarantee that matters - that with + * UNDISPATCHED the emission is *never* lost - and the second test documents that the default gives no + * such guarantee by showing subscription has not happened yet when the call returns. + */ +class ResilientCollectStartTest { + + private val aapsLogger: AAPSLogger = mock() + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + @AfterEach fun tearDown() = scope.cancel() + + private fun busLikeFlow() = MutableSharedFlow( + extraBufferCapacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + @Test + fun `undispatched start receives an item emitted immediately after subscribing`() { + val bus = busLikeFlow() + val seen = mutableListOf() + + bus.collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { seen += it } + // No await, no delay: this is the exact pattern a converted RxBus subscription has to survive, + // and the whole point is that the subscribe already happened above. + val delivered = bus.tryEmit("event") + + assertThat(delivered).isTrue() + assertThat(bus.subscriptionCount.value).isEqualTo(1) + } + + @Test + fun `the default start has not subscribed yet when the call returns`() { + val bus = busLikeFlow() + + bus.collectResilient(scope, aapsLogger, LTag.CORE) { } + + // Nothing is collecting yet, so a replay-0 source would drop anything emitted right now. This + // is why the bus subscriptions ask for UNDISPATCHED and a StateFlow collector need not. + assertThat(bus.subscriptionCount.value).isEqualTo(0) + } + + @Test + fun `undispatched start has subscribed by the time the call returns`() { + val bus = busLikeFlow() + + bus.collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { } + + assertThat(bus.subscriptionCount.value).isEqualTo(1) + } +} diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt index 873ff6736f83..a6f704c45a4c 100644 --- a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ResilientCollect.kt @@ -4,12 +4,14 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch /** * Collects a long-lived [Flow] resiliently in [scope], logging under [tag]. @@ -30,12 +32,30 @@ import kotlinx.coroutines.flow.retryWhen * NOTE: this only addresses *exceptions*. Collection is sequential, so a [block] that never returns * (e.g. an awaited callback that is lost) still blocks all subsequent emissions without throwing — * bound such calls with a timeout (`withTimeoutOrNull`) at the call site. + * + * ### [start], and when you need it + * + * By default the collecting coroutine is *scheduled*, so it has not subscribed yet when this function + * returns. On a source that replays (a `StateFlow`, a DB observer) that is harmless - a late + * subscriber still gets the current value. + * + * On a source with **no replay** it is not harmless. `RxBus` publishes through a `MutableSharedFlow` + * with `replay = 0`, so anything emitted between this call and the collector actually starting is + * dropped, silently and without a trace. RxJava's `subscribe()` registered synchronously and had no + * such window, so a subscription converted from Rx acquires this gap unless it asks not to. + * + * Pass [CoroutineStart.UNDISPATCHED] there: the coroutine then runs on the calling thread up to its + * first suspension, which for a bare `collect` is exactly the subscribe, and resumes on [scope]'s + * dispatcher afterwards. Note this also means that if the source *does* emit during subscribe (a + * `StateFlow`'s current value), that first [block] runs on the caller's thread - which is why this is + * opt-in rather than the default. */ fun Flow.collectResilient( scope: CoroutineScope, aapsLogger: AAPSLogger, tag: LTag, restartDelayMs: Long = 1000L, + start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend (T) -> Unit ): Job = onEach { item -> @@ -56,4 +76,4 @@ fun Flow.collectResilient( true } } - .launchIn(scope) + .let { flow -> scope.launch(start = start) { flow.collect() } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index 4ebd389fafee..25648c3b1614 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -34,6 +34,7 @@ import app.aaps.plugins.sync.tidepool.keys.TidepoolLongNonKey import app.aaps.plugins.sync.tidepool.keys.TidepoolStringNonKey import app.aaps.plugins.sync.tidepool.utils.RateLimit import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -97,9 +98,9 @@ class TidepoolPlugin @Inject constructor( } // scope is Dispatchers.IO, matching the scheduler these subscriptions used before. rxBus.toFlow(EventTidepoolDoUpload::class.java) - .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { doUpload(EventTidepoolDoUpload::class.simpleName) } + .collectResilient(scope, aapsLogger, LTag.TIDEPOOL, start = CoroutineStart.UNDISPATCHED) { doUpload(EventTidepoolDoUpload::class.simpleName) } rxBus.toFlow(EventTidepoolStatus::class.java) - .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { event -> + .collectResilient(scope, aapsLogger, LTag.TIDEPOOL, start = CoroutineStart.UNDISPATCHED) { event -> tidepoolRepository.addLog(event.status) tidepoolRepository.updateConnectionStatus(authFlowOut.connectionStatus) // Pass to setup wizard diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 8abcd575e79e..f5ae295fce66 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -38,6 +38,7 @@ import app.aaps.core.ui.compose.icons.IcPluginTizen import app.aaps.plugins.sync.R import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -82,9 +83,9 @@ class TizenPlugin @Inject constructor( scope = newScope // newScope is Dispatchers.IO, matching the scheduler these subscriptions used before. rxBus.toFlow(EventLoopUpdateGui::class.java) - .collectResilient(newScope, aapsLogger, LTag.CORE) { sendData(it) } + .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { sendData(it) } rxBus.toFlow(EventAutosensCalculationFinished::class.java) - .collectResilient(newScope, aapsLogger, LTag.CORE) { sendData(it) } + .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { sendData(it) } bolusProgressData.state .collectResilient(newScope, aapsLogger, LTag.CORE) { state -> if (state != null && !state.isSMB) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 16c8ef709243..411c2b790af4 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -59,6 +59,7 @@ import app.aaps.plugins.sync.xdrip.keys.XdripLongKey import app.aaps.plugins.sync.xdrip.workers.XdripDataSyncWorker import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren @@ -128,7 +129,7 @@ class XdripPlugin @Inject constructor( handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) // scope is Dispatchers.IO, which is what observeOn(aapsSchedulers.io) gave these before. rxBus.toFlow(EventAppExit::class.java) - .collectResilient(scope, aapsLogger, LTag.XDRIP) { WorkManager.getInstance(context).cancelUniqueWork(XDRIP_JOB_NAME) } + .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { WorkManager.getInstance(context).cancelUniqueWork(XDRIP_JOB_NAME) } persistenceLayer.observeAnyChange() // HR/SC writes come from the watch; this plugin doesn't broadcast them — skip to avoid reconnect-flush storm. .filter { types -> types.any { it != HR::class && it != SC::class } } @@ -137,9 +138,9 @@ class XdripPlugin @Inject constructor( delayAndScheduleExecution("DB_CHANGED(${types.joinToString { it.simpleName ?: "?" }})") } rxBus.toFlow(EventAutosensCalculationFinished::class.java) - .collectResilient(scope, aapsLogger, LTag.XDRIP) { sendStatusLine() } + .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { sendStatusLine() } rxBus.toFlow(EventAppInitialized::class.java) - .collectResilient(scope, aapsLogger, LTag.XDRIP) { sendStatusLine() } + .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { sendStatusLine() } eventWorker = Executors.newSingleThreadScheduledExecutor() } From a6d423d58085e64d15ee768a47bc888fb3af83e3 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 12:49:18 +0200 Subject: [PATCH 085/146] DataHandlerMobile listens on Flow Retry of the conversion that failed before the subscription window was closed. The two reified helpers, onEvent and onEventSync, carry 36 subscriptions between them, so converting the helpers converts all of them at once. concatMapCompletable serialized same-type events; a Flow collector is sequential by construction, so that ordering survives without an operator, and one collector per type keeps different types independent as they were. UNDISPATCHED is doing real work here rather than being defensive. These subscribe from init on a replay-0 bus, which is the widest possible version of the window, and it is exactly why the first attempt failed with zero interactions rather than a late delivery. With it, the onEvent test passes untouched. The onEventSync test still needed its bare verify turned into a timeout verify. That is not the same problem: delivery is guaranteed now, but the handler runs on Dispatchers.IO rather than inline on the posting thread, which is what the test's trampoline scheduler used to give it. Production never had that - the Rx version was observeOn(aapsSchedulers.io). The two ActionHeartRate/ActionStepsRate subscriptions stay on Rx. They use publish/buffer/debounce for quiet-period batching of reconnect-flush bursts, which has no direct Flow equivalent and wants its own operator and tests. 98 toObservable sites left. --- .../wear/wearintegration/DataHandlerMobile.kt | 44 +++++++++++-------- .../DataHandlerMobileWearBolusTest.kt | 9 +++- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index b20a77f4c740..eecdb5b7ab41 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -53,6 +53,7 @@ import app.aaps.core.interfaces.receivers.ReceiverStatusStore import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.rx.events.EventWearUpdateGui @@ -99,6 +100,10 @@ import app.aaps.core.ui.compose.LightGeneralColors import app.aaps.plugins.sync.R import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.rx3.rxCompletable import java.text.DateFormat import java.text.SimpleDateFormat @@ -160,6 +165,11 @@ class DataHandlerMobile @Inject constructor( @Inject lateinit var sceneActions: SceneActions private val disposable = CompositeDisposable() + // App lifetime, matching the disposable above: this is a @Singleton that subscribes in init and + // never tears down. Dispatchers.IO because that is what aapsSchedulers.io gave these handlers, and + // they do database and broadcast work - the Default pool would be the wrong one. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + /** * Registers a serialized suspend [handler] for one [EventData] subtype arriving from Wear. * @@ -168,18 +178,18 @@ class DataHandlerMobile @Inject constructor( * are logged and swallowed so a single failing event can't tear the subscription down. */ private inline fun onEvent(crossinline handler: suspend (T) -> Unit) { - disposable += rxBus - .toObservable(T::class.java) - .observeOn(aapsSchedulers.io) - .concatMapCompletable { event -> - rxCompletable { - aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${event.sourceNodeId}") - handler(event) - } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() + // concatMapCompletable serialized same-type events; a Flow collector is sequential by + // construction, so that ordering survives without an operator, and one collector per type keeps + // different types independent as before. collectResilient logs and continues, which is what + // doOnError + onErrorComplete did. + // + // UNDISPATCHED is required, not cosmetic: these subscribe from init on a replay-0 bus, so a + // scheduled collector would drop anything sent before it started. + rxBus.toFlow(T::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> + aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${event.sourceNodeId}") + handler(event) } - .subscribe() } /** @@ -191,13 +201,11 @@ class DataHandlerMobile @Inject constructor( crossinline detail: (T) -> String = { "" }, crossinline handler: (T) -> Unit ) { - disposable += rxBus - .toObservable(T::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${it.sourceNodeId}${detail(it)}") - handler(it) - }, fabricPrivacy::logException) + rxBus.toFlow(T::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${it.sourceNodeId}${detail(it)}") + handler(it) + } } init { diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt index 9973bb14d3b4..87e22303324b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt @@ -495,10 +495,15 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { // wired (a dropped subscription is a mechanical refactor error the compiler can't catch). @Test fun `onEventSync dispatches a posted SnoozeAlert to its handler`() { - // onEventSync subscribes directly; the trampoline io scheduler runs it inline. + // Was a bare verify: the test's trampoline io scheduler ran the Rx subscription inline on the + // posting thread. onEventSync collects a Flow on Dispatchers.IO now, so the handler runs off + // the posting thread and this needs the timeout verify its onEvent siblings already use. + // Production behaviour is unchanged - observeOn(aapsSchedulers.io) was never inline there. + // Delivery itself is not racy: the collector subscribes UNDISPATCHED, so it is registered + // before send() is reached. rxBus.send(EventData.SnoozeAlert(0L)) - verify(uiInteraction).stopAlarm("Muted from wear") + verify(uiInteraction, timeout(2000)).stopAlarm("Muted from wear") } @Test fun `onEvent dispatches a posted ActionBolusPreCheck to the suspend handler`() { From 84489ce718f4729ba3d461264d0b279d61bbd3a0 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 13:09:42 +0200 Subject: [PATCH 086/146] WearPlugin and DataLayerListenerServiceMobile listen on Flow Finishes the listen-side conversion in plugins/sync apart from two batching subscriptions. Two of these needed more than the usual substitution, because not every Rx subscription here was on io: - WearPlugin's EventWearUpdateGui observed on aapsSchedulers.main. It writes the watchface StateFlow the UI reads and then walks preferences, so the body is wrapped in withContext(Dispatchers.Main) rather than moving the collector - the other five subscriptions in that file do want IO. - DataLayerListenerServiceMobile has a Main.immediate scope that onDestroy already cancels, so it is the right lifetime, but its two subscriptions were on io and sendMessage talks to the Wear Data Layer. The collectors use the service scope and the sends go back to IO. All eight subscribe UNDISPATCHED: the bus has no replay, so a scheduled collector can miss anything sent before it starts. Still on Rx in this module: ActionHeartRate and ActionStepsRate in DataHandlerMobile. They use publish/buffer/debounce to coalesce Wear reconnect-flush bursts, which needs a quiet-period Flow operator and its own tests rather than an inline translation. CompositeDisposable, AapsSchedulers and FabricPrivacy are gone from both files. The WearPlugin constructor change shifted WearPluginTest's positional arguments. 90 toObservable sites left repo-wide, 5 in plugins/sync. --- .../app/aaps/plugins/sync/wear/WearPlugin.kt | 84 ++++++------------- .../DataLayerListenerServiceMobile.kt | 28 +++---- .../aaps/plugins/sync/wear/WearPluginTest.kt | 2 +- 3 files changed, 40 insertions(+), 74 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt index d7ee6b9bca0e..7ed28fc89f13 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt @@ -17,7 +17,6 @@ import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.pump.BolusProgressData import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished @@ -30,7 +29,6 @@ import app.aaps.core.interfaces.rx.weardata.CwfData import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.scenes.SceneAutomationApi -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.IntKey @@ -44,9 +42,8 @@ import app.aaps.plugins.sync.wear.receivers.WearDataReceiver import app.aaps.plugins.sync.wear.wearintegration.DataHandlerMobile import app.aaps.plugins.sync.wear.wearintegration.DataLayerListenerServiceMobileHelper import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob @@ -58,7 +55,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.rx3.rxCompletable +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -66,9 +63,7 @@ import javax.inject.Singleton class WearPlugin @Inject constructor( aapsLogger: AAPSLogger, rh: ResourceHelper, - private val aapsSchedulers: AapsSchedulers, preferences: Preferences, - private val fabricPrivacy: FabricPrivacy, private val rxBus: RxBus, private val context: Context, private val dataHandlerMobile: DataHandlerMobile, @@ -88,7 +83,6 @@ class WearPlugin @Inject constructor( aapsLogger = aapsLogger, rh = rh, preferences = preferences ) { - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null private val deferredStart = DeferredForegroundStart() @@ -152,37 +146,16 @@ class WearPlugin @Inject constructor( dataHandlerMobile.resendData("PreferenceChange") checkCustomWatchfacePreferences() } - disposable += rxBus - .toObservable(EventAutosensCalculationFinished::class.java) - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { dataHandlerMobile.resendData("EventAutosensCalculationFinished") } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() - } - .subscribe() - disposable += rxBus - .toObservable(EventLoopUpdateGui::class.java) - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { dataHandlerMobile.resendData("EventLoopUpdateGui") } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() - } - .subscribe() + rxBus.toFlow(EventAutosensCalculationFinished::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventAutosensCalculationFinished") } + rxBus.toFlow(EventLoopUpdateGui::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventLoopUpdateGui") } // AAPSCLIENT: fresh predictions arrive via NS devicestatus, not a local loop run — without this the // watch graph trails the phone by one loop cycle (the BG-triggered autosens resend fires BEFORE the // master's new devicestatus lands). Event is only sent on AAPSCLIENT; processedDeviceStatusData is // updated synchronously before it fires, so the resend reads the new predictions. - disposable += rxBus - .toObservable(EventNsClientStatusUpdated::class.java) - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { dataHandlerMobile.resendData("EventNsClientStatusUpdated") } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() - } - .subscribe() + rxBus.toFlow(EventNsClientStatusUpdated::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventNsClientStatusUpdated") } // Push status to watch quickly when a TT changes, without waiting for the loop's 10s debounce persistenceLayer.observeChanges() .drop(1) // Skip initial emission on collection start @@ -195,30 +168,24 @@ class WearPlugin @Inject constructor( // Push active-scene flag to wear so the tile can swap between scene list and STOP button scenes.activeFlow .collectResilient(newScope, aapsLogger, LTag.WEAR) { dataHandlerMobile.sendActiveSceneState(it) } - disposable += rxBus - .toObservable(EventWearUpdateTiles::class.java) - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { dataHandlerMobile.sendUserActions() } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() + rxBus.toFlow(EventWearUpdateTiles::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.sendUserActions() } + rxBus.toFlow(EventWearUpdateGui::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> + // This one observed on aapsSchedulers.main, not io: it writes the watchface StateFlow the + // UI reads and then walks preferences. newScope is IO, so the body is put back on main + // rather than the collector being moved - the other subscriptions here want IO. + withContext(Dispatchers.Main) { + event.customWatchfaceData?.let { cwf -> + if (!event.exportFile) { + _savedCustomWatchface.value = cwf + checkCustomWatchfacePreferences() + } + } + } } - .subscribe({}) - disposable += rxBus - .toObservable(EventWearUpdateGui::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ - it.customWatchfaceData?.let { cwf -> - if (!it.exportFile) { - _savedCustomWatchface.value = cwf - checkCustomWatchfacePreferences() - } - } - }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventMobileToWear::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { + rxBus.toFlow(EventMobileToWear::class.java) + .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { // If there is a broadcast selected (ie. // AAPSClient want pass data to AAPS // AAPSClient2 want pass data to AAPS or AAPSClient 1 @@ -253,7 +220,6 @@ class WearPlugin @Inject constructor( override suspend fun onStop() { scope?.cancel() scope = null - disposable.clear() deferredStart.cancel() super.onStop() dataLayerListenerServiceMobileHelper.stopService(context) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt index 082f59509d06..2f6ca0df605a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -8,8 +8,8 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventMobileToWearWatchface import app.aaps.core.interfaces.rx.events.EventWearUpdateGui @@ -27,15 +27,15 @@ import com.google.android.gms.wearable.PutDataMapRequest import com.google.android.gms.wearable.Wearable import com.google.android.gms.wearable.WearableListenerService import dagger.android.AndroidInjection -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import javax.inject.Inject @@ -48,7 +48,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { @Inject lateinit var wearPlugin: WearPlugin @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var rxBus: RxBus - @Inject lateinit var aapsSchedulers: AapsSchedulers inner class LocalBinder : Binder() { @@ -63,7 +62,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) - private val disposable = CompositeDisposable() private val rxPath get() = getString(app.aaps.core.interfaces.R.string.path_rx_bridge) private val rxWatchfacePath get() = getString(app.aaps.core.interfaces.R.string.path_rx_data_bridge) @@ -73,14 +71,17 @@ class DataLayerListenerServiceMobile : WearableListenerService() { super.onCreate() aapsLogger.debug(LTag.WEAR, "onCreate") handler.post { updateTranscriptionCapability() } - disposable += rxBus - .toObservable(EventMobileToWear::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { sendMessage(rxPath, it.payload.serialize()) } - disposable += rxBus - .toObservable(EventMobileToWearWatchface::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { sendMessage(rxWatchfacePath, it.payload) } + // scope is Main.immediate and onDestroy cancels it, so it is the right lifetime - but these two + // observed on aapsSchedulers.io, and sendMessage talks to the Wear Data Layer. So the collector + // lives on the service's scope and the send goes back to IO. + rxBus.toFlow(EventMobileToWear::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { + withContext(Dispatchers.IO) { sendMessage(rxPath, it.payload.serialize()) } + } + rxBus.toFlow(EventMobileToWearWatchface::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { + withContext(Dispatchers.IO) { sendMessage(rxWatchfacePath, it.payload) } + } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -91,7 +92,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { override fun onDestroy() { super.onDestroy() - disposable.clear() handler.removeCallbacksAndMessages(null) handler.looper.quitSafely() scope.cancel() diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt index 129de7351603..225b6c3685d9 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/WearPluginTest.kt @@ -24,6 +24,6 @@ class WearPluginTest : TestBaseWithProfile() { @BeforeEach fun prepare() { rateLimit = RateLimit(dateUtil) - wearPlugin = WearPlugin(aapsLogger, rh, aapsSchedulers, preferences, fabricPrivacy, rxBus, context, dataHandlerMobile, dataLayerListenerServiceMobileHelper, config, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), persistenceLayer, scenes) + wearPlugin = WearPlugin(aapsLogger, rh, preferences, rxBus, context, dataHandlerMobile, dataLayerListenerServiceMobileHelper, config, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), persistenceLayer, scenes) } } From 202ad40fbab99c92697910238aead33aac229d40 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 13:37:24 +0200 Subject: [PATCH 087/146] plugins/main listens on Flow Three files: PersistentNotificationPlugin, DummyService, IobCobCalculatorPlugin. The Android Auto subscription was an Observable.merge with a 10 second debounce. Flow's debounce means the same thing - emit once the source has been quiet for the period - so it is a direct swap. UNDISPATCHED is deliberately not claimed for that one: merge subscribes to its sources in child coroutines that are dispatched, so the window survives it. It does not matter there, because the worst case is a notification refresh a later event triggers anyway. The other seven do use UNDISPATCHED, and EventAppInitialized in IobCobCalculatorPlugin is the one that most needs it: it fires once, early, and a collector that has not started yet would miss it outright - the main calculation would then never be kicked off. PersistentNotificationPlugin and DummyService had no coroutine scope at all, so both gained one with the same lifetime the CompositeDisposable had - cancelled in onStop and onDestroy respectively. IobCobCalculatorPlugin already had an IO scope from its Flow collectors. HistoryBrowserData constructs IobCobCalculatorPlugin positionally, so removing the AapsSchedulers and FabricPrivacy parameters changed that call too. 59 toObservable sites left in main source. --- .../app/aaps/history/HistoryBrowserData.kt | 4 +- .../persistentNotification/DummyService.kt | 29 ++++---- .../PersistentNotificationPlugin.kt | 60 +++++++++-------- .../IobCobCalculatorPlugin.kt | 66 +++++++------------ 4 files changed, 75 insertions(+), 84 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/history/HistoryBrowserData.kt b/app/src/main/kotlin/app/aaps/history/HistoryBrowserData.kt index 22e36db15686..c64e7f78e737 100644 --- a/app/src/main/kotlin/app/aaps/history/HistoryBrowserData.kt +++ b/app/src/main/kotlin/app/aaps/history/HistoryBrowserData.kt @@ -55,8 +55,8 @@ class HistoryBrowserData @Inject constructor( ) override val iobCobCalculator: IobCobCalculator = IobCobCalculatorPlugin( - aapsLogger, aapsSchedulers, rxBus, preferences, rh, profileFunction, activePlugin, - fabricPrivacy, dateUtil, persistenceLayer, overviewData, calculationWorkflow, decimalFormatter, processedTbrEbData, + aapsLogger, rxBus, preferences, rh, profileFunction, activePlugin, + dateUtil, persistenceLayer, overviewData, calculationWorkflow, decimalFormatter, processedTbrEbData, signals ) { cache } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt index 82941416e198..ebec21153e96 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt @@ -7,12 +7,16 @@ import android.os.IBinder import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationHolder -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import dagger.android.DaggerService -import io.reactivex.rxjava3.disposables.CompositeDisposable +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import javax.inject.Inject /** @@ -20,13 +24,14 @@ import javax.inject.Inject */ class DummyService : DaggerService() { - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var rxBus: RxBus @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var notificationHolder: NotificationHolder - private val disposable = CompositeDisposable() + // App exit is the only thing this listens for, and the service dies with it - so an IO scope + // cancelled in onDestroy, matching what the CompositeDisposable did. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) inner class LocalBinder : Binder() { @@ -44,20 +49,16 @@ class DummyService : DaggerService() { } catch (e: Exception) { startForeground(4711, Notification()) } - disposable.add( - rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.CORE, "EventAppExit received") - stopSelf() - }, fabricPrivacy::logException) - ) + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.CORE, "EventAppExit received") + stopSelf() + } } override fun onDestroy() { aapsLogger.debug(LTag.CORE, "onDestroy") - disposable.clear() + scope.cancel() super.onDestroy() stopForeground(STOP_FOREGROUND_REMOVE) } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index e36ee80e8d16..27592110cf7b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -17,6 +17,7 @@ import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.iob.GlucoseStatusProvider import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationHolder import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.plugin.ActivePlugin @@ -25,7 +26,6 @@ import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished import app.aaps.core.interfaces.rx.events.EventInitializationChanged @@ -41,11 +41,17 @@ import app.aaps.core.objects.extensions.round import app.aaps.core.objects.extensions.toStringShort import app.aaps.core.utils.DeferredForegroundStart import app.aaps.plugins.main.R -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.runBlocking -import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton @@ -54,7 +60,6 @@ import javax.inject.Singleton class PersistentNotificationPlugin @Inject constructor( aapsLogger: AAPSLogger, rh: ResourceHelper, - private val aapsSchedulers: AapsSchedulers, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, private val fabricPrivacy: FabricPrivacy, @@ -95,39 +100,40 @@ class PersistentNotificationPlugin @Inject constructor( private val EXTRA_VOICE_REPLY = "extra_voice_reply" // End Android auto - private val disposable = CompositeDisposable() + private var scope: CoroutineScope? = null private val deferredStart = DeferredForegroundStart() private var lastAutoNotificationContent: String = "" + @OptIn(FlowPreview::class) override suspend fun onStart() { super.onStart() notificationHolder.createNotificationChannel() - disposable += rxBus - .toObservable(EventRefreshOverview::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ triggerNotificationUpdate() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventInitializationChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ triggerNotificationUpdate() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventAutosensCalculationFinished::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ triggerNotificationUpdate() }, fabricPrivacy::logException) + val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventRefreshOverview::class.java) + .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } + rxBus.toFlow(EventInitializationChanged::class.java) + .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } + rxBus.toFlow(EventAutosensCalculationFinished::class.java) + .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } /// Android Auto - debounced to prevent rapid pop-ups - disposable += Observable.merge( - rxBus.toObservable(EventRefreshOverview::class.java).map { }, - rxBus.toObservable(EventInitializationChanged::class.java).map { }, - rxBus.toObservable(EventAutosensCalculationFinished::class.java).map { } + // Flow's debounce means the same thing as Rx's: emit once the source has been quiet for the + // period. UNDISPATCHED is not claimed here - merge subscribes to its sources in child + // coroutines that are dispatched, so the window survives it. Harmless for this one: the worst + // case is a notification refresh that a later event triggers anyway. + merge( + rxBus.toFlow(EventRefreshOverview::class.java).map { }, + rxBus.toFlow(EventInitializationChanged::class.java).map { }, + rxBus.toFlow(EventAutosensCalculationFinished::class.java).map { } ) - .debounce(10, TimeUnit.SECONDS) - .observeOn(aapsSchedulers.io) - .subscribe({ triggerNotificationUpdate(includeAuto = true) }, fabricPrivacy::logException) + .debounce(10_000L) + .collectResilient(newScope, aapsLogger, LTag.CORE) { triggerNotificationUpdate(includeAuto = true) } /// End Android Auto } override suspend fun onStop() { - disposable.clear() + scope?.cancel() + scope = null deferredStart.cancel() dummyServiceHelper.stopService(context) super.onStop() diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt index a8dd352b26b1..94fc8978fe91 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt @@ -32,7 +32,6 @@ import app.aaps.core.interfaces.profile.EffectiveProfile import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAppInitialized import app.aaps.core.interfaces.rx.events.EventCalibrationChanged @@ -40,7 +39,6 @@ import app.aaps.core.interfaces.rx.events.EventConfigBuilderChange import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.MidnightTime -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.interfaces.workflow.CalculationSignalsEmitter import app.aaps.core.interfaces.workflow.CalculationWorkflow import app.aaps.core.keys.DoubleKey @@ -54,8 +52,8 @@ import app.aaps.core.objects.extensions.plus import app.aaps.core.objects.extensions.round import app.aaps.plugins.main.R import app.aaps.plugins.main.iob.iobCobCalculator.data.AutosensDataStoreObject -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -79,13 +77,11 @@ import kotlin.math.min @Singleton class IobCobCalculatorPlugin @Inject constructor( aapsLogger: AAPSLogger, - private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, private val preferences: Preferences, rh: ResourceHelper, private val profileFunction: ProfileFunction, private val activePlugin: ActivePlugin, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, private val persistenceLayer: PersistenceLayer, private val overviewData: OverviewData, @@ -106,7 +102,6 @@ class IobCobCalculatorPlugin @Inject constructor( aapsLogger, rh ), IobCobCalculator { - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null private var iobTable = LongSparseArray() // oldest at index 0 @@ -121,23 +116,16 @@ class IobCobCalculatorPlugin @Inject constructor( val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope // EventConfigBuilderChange - disposable += rxBus - .toObservable(EventConfigBuilderChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ resetDataAndRunCalculation("onEventConfigBuilderChange") }, fabricPrivacy::logException) + rxBus.toFlow(EventConfigBuilderChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { resetDataAndRunCalculation("onEventConfigBuilderChange") } // EventCalibrationChanged → the fit changed, so bucketed data needs to be re-smoothed // with the new calibration applied. scheduleHistoryDataChange has its own 5s debounce // so bursts (delete-many, bulk-add) collapse into one workflow run. - disposable += rxBus - .toObservable(EventCalibrationChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe( - { - val invalidateFrom = System.currentTimeMillis() - T.hours(24).msecs() - scheduleHistoryDataChange(invalidateFrom, reloadBgData = true, triggeredByNewBG = false) - }, - fabricPrivacy::logException - ) + rxBus.toFlow(EventCalibrationChanged::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { + val invalidateFrom = System.currentTimeMillis() - T.hours(24).msecs() + scheduleHistoryDataChange(invalidateFrom, reloadBgData = true, triggeredByNewBG = false) + } // EffectiveProfileSwitch changes persistenceLayer.observeChanges(EPS::class.java) .onEach { epsList -> @@ -184,32 +172,28 @@ class IobCobCalculatorPlugin @Inject constructor( .onEach { scheduleHistoryDataChange(0, reloadBgData = true) }.launchIn(newScope) - disposable += rxBus - .toObservable(EventAppInitialized::class.java) - .observeOn(aapsSchedulers.io) - .subscribe( - { - calculationWorkflow.runCalculation( - CalculationWorkflow.MAIN_CALCULATION, - this, - overviewData, - cache.get(), - signals, - "onEventAppInitialized", - System.currentTimeMillis(), - bgDataReload = true, - triggeredByNewBG = false - ) - }, - fabricPrivacy::logException - ) + // EventAppInitialized fires once, early. UNDISPATCHED matters most here of the three: a + // scheduled collector could miss it outright and the main calculation would never be kicked off. + rxBus.toFlow(EventAppInitialized::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { + calculationWorkflow.runCalculation( + CalculationWorkflow.MAIN_CALCULATION, + this, + overviewData, + cache.get(), + signals, + "onEventAppInitialized", + System.currentTimeMillis(), + bgDataReload = true, + triggeredByNewBG = false + ) + } historyWorker = Executors.newSingleThreadScheduledExecutor() } override suspend fun onStop() { scope?.cancel() scope = null - disposable.clear() historyWorker?.shutdown() historyWorker = null super.onStop() From f42e953e0f3137085119eaaab826ea4664747e39 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 15:54:15 +0200 Subject: [PATCH 088/146] plugins/automation and plugins/configuration listen on Flow Two things here were not the usual substitution. AutomationRuntime's two subscriptions called scope.launch { processActions() } from inside the Rx callback, so rule processing could overlap. A Flow collector is sequential, so calling processActions() inline would have serialized it. That may well be better, but changing when automation rules can run concurrently is not something to do as a side effect of swapping the subscription mechanism, so the launch stays. The setup wizard subscriptions live in Compose, so they become LaunchedEffect + collect rather than collectResilient. LaunchedEffect runs in the composition's scope on Main, which is what observeOn(mainThread()) gave them, and it cancels when the screen leaves - exactly what onDispose was doing. Neither had an Rx error handler, so nothing is lost by not wrapping them. SWDefinition uses plain onEach/launchIn on the injected ApplicationScope: its CompositeDisposable was never cleared, so that subscription was already app-lifetime, and the body is a bus send with no logger in scope to report to. AutomationStateHolder and AutomationComposeContent needed an AAPSLogger to call collectResilient. Both already carried AapsSchedulers and FabricPrivacy purely for the Rx subscription, so those two are swapped for the one logger rather than added on top. 53 toObservable sites left in main source. --- .../plugins/automation/AutomationRuntime.kt | 41 +++++++++---------- .../compose/AutomationComposeContent.kt | 8 ++-- .../compose/AutomationStateHolder.kt | 31 ++++++-------- .../automation/services/LocationService.kt | 27 ++++++------ .../configuration/setupwizard/SWDefinition.kt | 16 ++++---- .../setupwizard/SWEventListener.kt | 16 ++++---- .../setupwizard/SetupWizardScreen.kt | 30 ++++++-------- 7 files changed, 78 insertions(+), 91 deletions(-) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt index 2cb42b027863..c36d539b3b36 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt @@ -85,8 +85,8 @@ import app.aaps.plugins.automation.triggers.TriggerTime import app.aaps.plugins.automation.triggers.TriggerTimeRange import app.aaps.plugins.automation.triggers.TriggerWifiSsid import dagger.android.HasAndroidInjector -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -159,15 +159,13 @@ class AutomationRuntime @Inject constructor( AutomationComposeContent( plugin = this, rxBus = rxBus, - aapsSchedulers = aapsSchedulers, - fabricPrivacy = fabricPrivacy, + aapsLogger = aapsLogger, injector = injector, uel = uel, profileRepository = profileRepository, sceneApi = sceneApi ) - private var disposable: CompositeDisposable = CompositeDisposable() private var scope: CoroutineScope? = null private val deferredStart = DeferredForegroundStart() @@ -355,28 +353,29 @@ class AutomationRuntime @Inject constructor( updateLocationService() }.launchIn(newScope) - disposable += rxBus - .toObservable(EventLocationChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.AUTOMATION, "Grabbed location: ${it.location.latitude} ${it.location.longitude} Provider: ${it.location.provider}") - scope?.launch { processActions() } - }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventBTChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.AUTOMATION, "Grabbed new BT event: $it") - btConnects.add(it) - scope?.launch { processActions() } - }, fabricPrivacy::logException) + // processActions() stays launched rather than called inline. A Flow collector is sequential, so + // calling it directly would serialize rule processing, which the Rx version did not do - it + // fired scope.launch and returned. That may well be an improvement, but changing when + // automation rules can run concurrently is not something to do as a side effect of swapping + // the subscription mechanism. + rxBus.toFlow(EventLocationChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.AUTOMATION, "Grabbed location: ${it.location.latitude} ${it.location.longitude} Provider: ${it.location.provider}") + scope?.launch { processActions() } + } + rxBus.toFlow(EventBTChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.AUTOMATION, "Grabbed new BT event: $it") + btConnects.add(it) + scope?.launch { processActions() } + } } /** Tear down the runtime. Not called in production (always-on singleton); used by tests. */ fun stop() { scope?.cancel() scope = null - disposable.clear() + deferredStart.cancel() if (locationServiceRunning) { locationServiceHelper.stopService(context) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationComposeContent.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationComposeContent.kt index 186ab09428f2..4db7a6424742 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationComposeContent.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationComposeContent.kt @@ -32,10 +32,9 @@ import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.logging.UserEntryLogger import app.aaps.core.interfaces.profile.ProfileRepository -import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.scenes.SceneAutomationApi -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.objects.extensions.profileNames import app.aaps.core.ui.compose.ComposablePluginContent import app.aaps.core.ui.compose.ToolbarConfig @@ -54,8 +53,7 @@ import kotlin.reflect.full.primaryConstructor class AutomationComposeContent( private val plugin: AutomationRuntime, private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, - private val fabricPrivacy: FabricPrivacy, + private val aapsLogger: AAPSLogger, private val injector: HasAndroidInjector, private val uel: UserEntryLogger, private val profileRepository: ProfileRepository, @@ -69,7 +67,7 @@ class AutomationComposeContent( onSettings: (() -> Unit)? ) { val holder = remember { - AutomationStateHolder(plugin, rxBus, aapsSchedulers, fabricPrivacy, injector) + AutomationStateHolder(plugin, rxBus, aapsLogger, injector) } DisposableEffect(holder) { holder.start() diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt index 79fc7997b970..d1bcec048b17 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt @@ -1,8 +1,8 @@ package app.aaps.plugins.automation.compose -import app.aaps.core.interfaces.rx.AapsSchedulers +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.rx.bus.RxBus -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.ui.compose.icons.IcUserOptions import app.aaps.core.interfaces.navigation.ElementType import app.aaps.plugins.automation.AutomationEventObject @@ -12,8 +12,8 @@ import app.aaps.plugins.automation.events.EventAutomationUpdateGui import app.aaps.plugins.automation.triggers.TriggerConnector import app.aaps.plugins.automation.triggers.TriggerLocation import dagger.android.HasAndroidInjector -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -28,8 +28,7 @@ import kotlinx.coroutines.flow.onEach class AutomationStateHolder( private val plugin: AutomationRuntime, private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, - private val fabricPrivacy: FabricPrivacy, + private val aapsLogger: AAPSLogger, private val injector: HasAndroidInjector ) { @@ -42,7 +41,6 @@ class AutomationStateHolder( private val _editState = MutableStateFlow(AutomationEditUiState()) val editState: StateFlow = _editState.asStateFlow() - private var disposable: CompositeDisposable? = null private var scope: CoroutineScope? = null // Working copy for edit @@ -50,15 +48,7 @@ class AutomationStateHolder( private var workingPosition: Int = -1 fun start() { - if (disposable != null) return - val d = CompositeDisposable() - d += rxBus.toObservable(EventAutomationUpdateGui::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ - refresh() - refreshEditState() - }, fabricPrivacy::logException) - disposable = d + if (scope != null) return // drop(1) skips the seed empty snapshot the plugin emits before loadFromSP runs — the // refresh() below covers the cold start, and the plugin's first real emission after load // re-triggers it. EventWearUpdateTiles is now broadcast from the plugin's own scope so it @@ -66,13 +56,18 @@ class AutomationStateHolder( // gated on this holder being alive. val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) scope = newScope + // This one observed on aapsSchedulers.main and the scope is already Main, so the dispatcher + // matches without any extra step - it touches Compose state. + rxBus.toFlow(EventAutomationUpdateGui::class.java) + .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { + refresh() + refreshEditState() + } plugin.events.drop(1).onEach { refresh() }.launchIn(newScope) refresh() } fun stop() { - disposable?.clear() - disposable = null scope?.cancel() scope = null } diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt index 7180b0b39ca1..0f5f9fe21fb6 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt @@ -14,7 +14,6 @@ import app.aaps.core.data.time.T import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationHolder -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventShowSnackbar @@ -24,8 +23,12 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.plugins.automation.events.EventLocationChange import com.google.android.gms.location.LocationServices import dagger.android.DaggerService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import javax.inject.Inject class LocationService : DaggerService() { @@ -33,12 +36,12 @@ class LocationService : DaggerService() { @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var rxBus: RxBus @Inject lateinit var preferences: Preferences - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var notificationHolder: NotificationHolder @Inject lateinit var lastLocationDataContainer: LastLocationDataContainer - private val disposable = CompositeDisposable() + // Replaces the CompositeDisposable: same lifetime, cancelled in onDestroy below. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var locationManager: LocationManager? = null private var locationListener: LocationListener? = null @@ -148,17 +151,16 @@ class LocationService : DaggerService() { rxBus.send(EventShowSnackbar(getString(app.aaps.core.ui.R.string.location_permission_not_granted), EventShowSnackbar.Type.Error)) } - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.LOCATION, "EventAppExit received") - stopSelf() - }, fabricPrivacy::logException) + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.LOCATION, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.LOCATION, "EventAppExit received") + stopSelf() + } } override fun onDestroy() { super.onDestroy() + scope.cancel() try { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) { return @@ -167,7 +169,6 @@ class LocationService : DaggerService() { } catch (ex: Exception) { aapsLogger.error(LTag.LOCATION, "fail to remove location listener, ignore", ex) } - disposable.clear() } private fun initializeLocationManager() { diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt index 78024f12c374..3f7851d735aa 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt @@ -45,9 +45,9 @@ import app.aaps.plugins.configuration.setupwizard.elements.SWPairingStatus import app.aaps.plugins.configuration.setupwizard.elements.SWPermissions import app.aaps.plugins.configuration.setupwizard.elements.SWPlugin import app.aaps.plugins.configuration.setupwizard.elements.SWRadioButton -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import javax.inject.Inject @@ -102,7 +102,6 @@ class SWDefinition @Inject constructor( var onRequestPermission: ((PermissionGroup) -> Unit)? = null var permissionItems: (() -> List>)? = null var isDirectoryAccessGranted: (() -> Boolean)? = null - private val disposable = CompositeDisposable() private val screens: MutableList = ArrayList() private fun pluginOption(pType: PluginType, @androidx.annotation.StringRes description: Int): SWPlugin = @@ -118,10 +117,13 @@ class SWDefinition @Inject constructor( config.PUMPCONTROL -> swDefinitionPumpControl() config.AAPSCLIENT -> swDefinitionNSClient() } - disposable += rxBus - .toObservable(EventConfigBuilderChange::class.java) - .observeOn(aapsSchedulers.main) - .subscribe { rxBus.send(EventSWUpdate(true)) } + // appScope rather than a new one: the CompositeDisposable this replaces was never cleared, + // so this subscription was already app-lifetime. Plain onEach/launchIn rather than + // collectResilient because there is no logger here and the body is a bus send that cannot + // meaningfully fail - the Rx version had no error handler either. + rxBus.toFlow(EventConfigBuilderChange::class.java) + .onEach { rxBus.send(EventSWUpdate(true)) } + .launchIn(appScope) } return screens } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt index 3e98ca1035f7..20716b51b97f 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt @@ -3,6 +3,7 @@ package app.aaps.plugins.configuration.setupwizard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource @@ -15,7 +16,6 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.stringResource import app.aaps.plugins.configuration.setupwizard.elements.SWItem -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import javax.inject.Inject class SWEventListener @Inject constructor( @@ -61,14 +61,12 @@ class SWEventListener @Inject constructor( // the Composable. That keeps the resolving out of the Rx callback, which had to reach for a // Context purely to read a string. val statusState = remember { mutableStateOf(TextRef.Literal(status)) } - DisposableEffect(clazz) { - val disposable = rxBus - .toObservable(clazz) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { event -> - statusState.value = event.getStatus() - } - onDispose { disposable.dispose() } + LaunchedEffect(clazz) { + // Composition scope is Main, matching observeOn(mainThread()), and it is cancelled when + // this leaves the composition - what onDispose did. + rxBus.toFlow(clazz).collect { event -> + statusState.value = event.getStatus() + } } val labelText = textLabel?.let { stringResource(it) } ?: "" Text(text = "$labelText ${stringResource(statusState.value)}".trim()) diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt index 78954528679b..915946ac1cd1 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.merge import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -39,9 +41,6 @@ import app.aaps.core.ui.compose.pump.StepProgressIndicator import app.aaps.core.ui.compose.pump.WizardButton import app.aaps.core.ui.compose.pump.WizardStepLayout import app.aaps.plugins.configuration.R -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -108,21 +107,16 @@ fun SetupWizardScreen( // Trigger recomposition on RxBus events var updateTick by remember { mutableIntStateOf(0) } - DisposableEffect(Unit) { - val disposable = CompositeDisposable() - disposable += rxBus.toObservable(EventSWUpdate::class.java) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateTick++ } - disposable += rxBus.toObservable(EventPumpStatusChanged::class.java) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateTick++ } - disposable += rxBus.toObservable(EventSWRLStatus::class.java) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateTick++ } - disposable += rxBus.toObservable(EventSWSyncStatus::class.java) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateTick++ } - onDispose { disposable.clear() } + // LaunchedEffect rather than DisposableEffect + CompositeDisposable: it runs in the composition's + // scope on Main, which is what observeOn(mainThread()) gave these, and it is cancelled when the + // screen leaves - the same thing onDispose was doing. + LaunchedEffect(Unit) { + merge( + rxBus.toFlow(EventSWUpdate::class.java), + rxBus.toFlow(EventPumpStatusChanged::class.java), + rxBus.toFlow(EventSWRLStatus::class.java), + rxBus.toFlow(EventSWSyncStatus::class.java) + ).collect { updateTick++ } } // Read updateTick to subscribe to recomposition From fe80b5c1ea0d50968f6c6ec22dbc337622270acd Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 16:12:41 +0200 Subject: [PATCH 089/146] Wear: listen with Flow instead of Rx Observable Converts the last 8 rxBus.toObservable subscriptions in the wear app to rxBus.toFlow(...).collectResilient(...). Wear stays an Android-only app, but it uses the same RxBus interface from :core:interfaces, so its calls are what keep toObservable (and io.reactivex.Observable) on that shared interface. - DataHandlerWear: the reified onEvent helper now collects, so all 23 per-type subscriptions move with it. - DataLayerListenerServiceWear: 3 subscriptions. The two that send to the Data Layer keep their IO thread with withContext(Dispatchers.IO); the preference one stays on the Main scope where it was. Its CompositeDisposable stays - it holds the heart-rate and step-count listeners, not bus subscriptions. - LoopStatusActivity, MenuListActivity: collect on lifecycleScope. - CircleWatchface, BaseWatchFace: collect on watchfaceScope. All of them use CoroutineStart.UNDISPATCHED, because RxBus has no replay: a scheduled collector could miss events sent before it starts, while subscribe() used to register right away. Dead CompositeDisposable and AapsSchedulers members and their imports are removed where nothing else used them. Tests: DataHandlerWearTest lost the synchronous delivery the trampoline scheduler gave it - the handler now runs on the collector's IO dispatcher. It waits with verify(timeout) and a CompletableDeferred instead. --- .../app/aaps/wear/comm/DataHandlerWear.kt | 16 ++++---- .../wear/comm/DataLayerListenerServiceWear.kt | 28 +++++++------- .../activities/LoopStatusActivity.kt | 32 +++++++++------- .../interaction/utils/MenuListActivity.kt | 20 +++++----- .../aaps/wear/watchfaces/CircleWatchface.kt | 15 +++----- .../wear/watchfaces/utils/BaseWatchFace.kt | 15 +++----- .../app/aaps/wear/comm/DataHandlerWearTest.kt | 38 +++++++++++++------ 7 files changed, 86 insertions(+), 78 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt index 29c296be962d..d5b9915d8dc4 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt @@ -16,8 +16,9 @@ import androidx.wear.tiles.TileService import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import kotlinx.coroutines.CoroutineStart +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventWearDataToMobile import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData @@ -61,8 +62,6 @@ import app.aaps.wear.tile.SceneTileService import app.aaps.wear.tile.TempTargetTileService import app.aaps.wear.tile.UserActionTileService import com.google.android.gms.wearable.WearableListenerService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.json.Json import kotlinx.coroutines.Dispatchers @@ -75,7 +74,6 @@ import javax.inject.Singleton class DataHandlerWear @Inject constructor( private val context: Context, private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, private val sp: SP, private val preferences: Preferences, private val aapsLogger: AAPSLogger, @@ -85,7 +83,6 @@ class DataHandlerWear @Inject constructor( // Coroutine scope for DataStore operations private val dataStoreScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val disposable = CompositeDisposable() init { setupBus() @@ -103,10 +100,11 @@ class DataHandlerWear @Inject constructor( crossinline detail: (T) -> String = { "" }, crossinline handler: (T) -> Unit ) { - disposable += rxBus - .toObservable(T::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { event -> + // dataStoreScope is Dispatchers.IO, matching observeOn(aapsSchedulers.io). UNDISPATCHED because + // these subscribe from setupBus() on a replay-0 bus: a scheduled collector could miss anything + // sent before it started. + rxBus.toFlow(T::class.java) + .collectResilient(dataStoreScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${event.sourceNodeId}${detail(event)}") handler(event) } diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 0c6148c9d5d1..8104caefb73d 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -12,6 +12,8 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import kotlinx.coroutines.CoroutineStart +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventWearDataToMobile import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData @@ -36,6 +38,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -72,22 +75,19 @@ class DataLayerListenerServiceWear : WearableListenerService() { super.onCreate() startForegroundService() handler.post { updateTranscriptionCapability() } - disposable += rxBus - .toObservable(EventWearToMobile::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { - sendMessage(rxPath, it.payload.serialize()) + // scope is Main.immediate, which is the right lifetime. The two sends observed on io and talk + // to the Data Layer, so those bodies go back to IO; the preference one observed on main and + // touches the listeners, so it stays where the collector is. + rxBus.toFlow(EventWearToMobile::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { + withContext(Dispatchers.IO) { sendMessage(rxPath, it.payload.serialize()) } } - disposable += rxBus - .toObservable(EventWearDataToMobile::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { - sendMessage(rxDataPath, it.payload.serializeByte()) + rxBus.toFlow(EventWearDataToMobile::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { + withContext(Dispatchers.IO) { sendMessage(rxDataPath, it.payload.serializeByte()) } } - disposable += rxBus - .toObservable(EventWearPreferenceChange::class.java) - .observeOn(aapsSchedulers.main) - .subscribe { event: EventWearPreferenceChange -> + rxBus.toFlow(EventWearPreferenceChange::class.java) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> if (event.changedKey == getString(R.string.key_heart_rate_sampling)) updateHeartRateListener() if (event.changedKey == getString(R.string.key_steps_sampling)) updateStepsCountListener() } diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt index 29c430605655..5853ef3ddf34 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt @@ -47,7 +47,11 @@ import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CancellationException import app.aaps.core.interfaces.rx.bus.RxBus +import kotlinx.coroutines.CoroutineStart +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.LoopStatusData @@ -71,8 +75,6 @@ import app.aaps.wear.interaction.actions.WearSecondaryText import app.aaps.wear.interaction.actions.WearSummaryCardBg import app.aaps.wear.interaction.actions.formatDurationMinutes import dagger.android.AndroidInjection -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import java.util.Date import javax.inject.Inject import kotlin.math.abs @@ -119,7 +121,6 @@ class LoopStatusActivity : AppCompatActivity() { @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var dateUtil: DateUtil - private val disposable = CompositeDisposable() private var uiState by mutableStateOf(LoopStatusUiState.Loading) override fun onCreate(savedInstanceState: Bundle?) { @@ -136,15 +137,21 @@ class LoopStatusActivity : AppCompatActivity() { } } - disposable += rxBus - .toObservable(EventData.LoopStatusResponse::class.java) - .subscribe({ event -> - aapsLogger.debug(LTag.WEAR, "Received loop status response") - runOnUiThread { uiState = LoopStatusUiState.Success(event.data) } - }, { error -> - aapsLogger.error(LTag.WEAR, "Error receiving loop status", error) - runOnUiThread { uiState = LoopStatusUiState.Error(getString(R.string.loop_status_error)) } - }) + // lifecycleScope is Main and dies with the activity, so runOnUiThread is no longer needed. + // The Rx onError put the screen into an error state rather than only logging, so that is kept + // explicitly - collectResilient on its own would log and carry on with the UI still spinning. + rxBus.toFlow(EventData.LoopStatusResponse::class.java) + .collectResilient(lifecycleScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> + try { + aapsLogger.debug(LTag.WEAR, "Received loop status response") + uiState = LoopStatusUiState.Success(event.data) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + aapsLogger.error(LTag.WEAR, "Error receiving loop status", e) + uiState = LoopStatusUiState.Error(getString(R.string.loop_status_error)) + } + } } override fun onResume() { @@ -154,7 +161,6 @@ class LoopStatusActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() - disposable.clear() } private fun requestLoopStatus() { diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt index 7e6a96a15d78..c3dba7b7f6ce 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt @@ -36,14 +36,16 @@ import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.ListHeader import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text -import app.aaps.core.interfaces.rx.AapsSchedulers +import androidx.lifecycle.lifecycleScope +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient +import kotlinx.coroutines.CoroutineStart import app.aaps.core.interfaces.rx.events.EventUpdateSelectedWatchface import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.keys.interfaces.Preferences import dagger.android.support.DaggerAppCompatActivity -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject abstract class MenuListActivity : DaggerAppCompatActivity() { @@ -51,10 +53,9 @@ abstract class MenuListActivity : DaggerAppCompatActivity() { @Inject lateinit var sp: SP @Inject lateinit var preferences: Preferences @Inject lateinit var rxBus: RxBus - @Inject lateinit var aapsSchedulers: AapsSchedulers + @Inject lateinit var aapsLogger: AAPSLogger private var elements by mutableStateOf>(emptyList()) - private val disposable = CompositeDisposable() protected abstract fun provideElements(): List protected abstract fun doAction(position: String) @@ -67,10 +68,10 @@ abstract class MenuListActivity : DaggerAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - disposable += rxBus - .toObservable(EventUpdateSelectedWatchface::class.java) - .observeOn(aapsSchedulers.main) - .subscribe { _: EventUpdateSelectedWatchface -> elements = provideElements() } + // lifecycleScope is Main, which is what observeOn(aapsSchedulers.main) supplied, and it dies + // with the activity like the CompositeDisposable did. + rxBus.toFlow(EventUpdateSelectedWatchface::class.java) + .collectResilient(lifecycleScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { elements = provideElements() } elements = provideElements() val menuTitle = title.toString() val titleIcon = provideTitleIcon() @@ -87,7 +88,6 @@ abstract class MenuListActivity : DaggerAppCompatActivity() { } override fun onDestroy() { - disposable.clear() super.onDestroy() } diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt index 5dcbd34e6749..620afd417235 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt @@ -14,8 +14,9 @@ import android.view.WindowManager import android.widget.TextView import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import kotlinx.coroutines.CoroutineStart +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventUpdateSelectedWatchface import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData @@ -30,8 +31,6 @@ import app.aaps.wear.watchfaces.utils.WatchFace import app.aaps.wear.watchfaces.utils.WatchFaceTime import app.aaps.wear.watchfaces.utils.WatchfaceViewAdapter.Companion.SelectedWatchFace import dagger.android.AndroidInjection -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -47,12 +46,10 @@ import kotlin.math.max class CircleWatchface : WatchFace() { @Inject lateinit var rxBus: RxBus - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var sp: SP @Inject lateinit var complicationDataRepository: app.aaps.wear.data.ComplicationDataRepository - private var disposable = CompositeDisposable() private val watchfaceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) // DataStore as single source of truth - using EventData models directly @@ -126,10 +123,9 @@ class CircleWatchface : WatchFace() { } } - disposable += rxBus - .toObservable(EventData.Preferences::class.java) - .observeOn(aapsSchedulers.main) - .subscribe { + // watchfaceScope is Main.immediate, matching observeOn(aapsSchedulers.main). + rxBus.toFlow(EventData.Preferences::class.java) + .collectResilient(watchfaceScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { if (myLayout != null) { // Only update if layout initialized prepareDrawTime() prepareLayout() @@ -142,7 +138,6 @@ class CircleWatchface : WatchFace() { } override fun onDestroy() { - disposable.clear() watchfaceScope.cancel() super.onDestroy() } diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt index 09469d342b65..88ef3785f12b 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt @@ -16,8 +16,9 @@ import android.view.WindowManager import androidx.viewbinding.ViewBinding import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import kotlinx.coroutines.CoroutineStart +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData.ActionResendData import app.aaps.core.interfaces.sharedPreferences.SP @@ -32,8 +33,6 @@ import app.aaps.wear.data.statusDataArray import app.aaps.wear.events.EventWearPreferenceChange import app.aaps.wear.interaction.menus.MainMenuActivity import dagger.android.AndroidInjection -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -50,12 +49,10 @@ abstract class BaseWatchFace : WatchFace() { @Inject lateinit var complicationDataRepository: ComplicationDataRepository @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var rxBus: RxBus - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var sp: SP @Inject lateinit var dateUtil: DateUtil @Inject lateinit var simpleUi: SimpleUi - private var disposable = CompositeDisposable() private val watchfaceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) // DataStore as single source of truth - using EventData models directly @@ -178,10 +175,9 @@ abstract class BaseWatchFace : WatchFace() { displayHeight = bounds.height() specW = View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY) specH = if (forceSquareCanvas) specW else View.MeasureSpec.makeMeasureSpec(displayHeight, View.MeasureSpec.EXACTLY) - disposable += rxBus - .toObservable(EventWearPreferenceChange::class.java) - .observeOn(aapsSchedulers.main) - .subscribe { _: EventWearPreferenceChange -> + // watchfaceScope is Main.immediate, matching observeOn(aapsSchedulers.main). + rxBus.toFlow(EventWearPreferenceChange::class.java) + .collectResilient(watchfaceScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { simpleUi.updatePreferences() if (::binding.isInitialized && layoutSet) setDataFields() invalidate() @@ -321,7 +317,6 @@ abstract class BaseWatchFace : WatchFace() { } override fun onDestroy() { - disposable.clear() watchfaceScope.cancel() simpleUi.onDestroy() super.onDestroy() diff --git a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt index 8c504353c5bf..3a4369870dce 100644 --- a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt @@ -14,9 +14,13 @@ import app.aaps.wear.WearTestBase import app.aaps.wear.data.ComplicationDataRepository import com.google.common.truth.Truth.assertThat import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock +import org.mockito.Mockito.timeout import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -40,7 +44,7 @@ internal class DataHandlerWearTest : WearTestBase() { fun setupHandler() { whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) rxBus = RxBusImpl(aapsSchedulers, logger) - sut = DataHandlerWear(context, rxBus, aapsSchedulers, sp, preferences, logger, complicationDataRepository) + sut = DataHandlerWear(context, rxBus, sp, preferences, logger, complicationDataRepository) } @Test @@ -48,25 +52,35 @@ internal class DataHandlerWearTest : WearTestBase() { // wearControl (false) equals the mock preferences default, so the tile-refresh branch is skipped. rxBus.send(EventData.Preferences(0L, false, true, 50, 80, 25.0, 0.5, 1.0, 5, 10)) - verify(sp).putBoolean(R.string.key_units_mgdl, true) - verify(sp).putInt(R.string.key_bolus_wizard_percentage, 50) - verify(sp).putInt(R.string.key_treatments_safety_max_carbs, 80) - verify(sp).putDouble(R.string.key_treatments_safety_max_bolus, 25.0) - verify(preferences).put(DoubleKey.OverviewInsulinButtonIncrement1, 0.5) - verify(preferences).put(DoubleKey.OverviewInsulinButtonIncrement2, 1.0) - verify(preferences).put(IntKey.OverviewCarbsButtonIncrement1, 5) - verify(preferences).put(IntKey.OverviewCarbsButtonIncrement2, 10) + // The handler now runs on the collector's IO dispatcher, so the writes land shortly after + // send() returns instead of on the caller thread. verify(timeout) waits for them. + verify(sp, timeout(HANDLER_TIMEOUT_MS)).putBoolean(R.string.key_units_mgdl, true) + verify(sp, timeout(HANDLER_TIMEOUT_MS)).putInt(R.string.key_bolus_wizard_percentage, 50) + verify(sp, timeout(HANDLER_TIMEOUT_MS)).putInt(R.string.key_treatments_safety_max_carbs, 80) + verify(sp, timeout(HANDLER_TIMEOUT_MS)).putDouble(R.string.key_treatments_safety_max_bolus, 25.0) + verify(preferences, timeout(HANDLER_TIMEOUT_MS)).put(DoubleKey.OverviewInsulinButtonIncrement1, 0.5) + verify(preferences, timeout(HANDLER_TIMEOUT_MS)).put(DoubleKey.OverviewInsulinButtonIncrement2, 1.0) + verify(preferences, timeout(HANDLER_TIMEOUT_MS)).put(IntKey.OverviewCarbsButtonIncrement1, 5) + verify(preferences, timeout(HANDLER_TIMEOUT_MS)).put(IntKey.OverviewCarbsButtonIncrement2, 10) } @Test fun `a ping is answered with a pong to the mobile`() { - var pong: EventData.ActionPong? = null + val pong = CompletableDeferred() rxBus.toObservable(EventWearToMobile::class.java).subscribe { evt -> - (evt.payload as? EventData.ActionPong)?.let { pong = it } + (evt.payload as? EventData.ActionPong)?.let { pong.complete(it) } } rxBus.send(EventData.ActionPing(1_000L)) - assertThat(pong).isNotNull() + // Answered from the handler's IO dispatcher, so wait for it rather than reading a field. + val answer = runBlocking { withTimeoutOrNull(HANDLER_TIMEOUT_MS) { pong.await() } } + assertThat(answer).isNotNull() + } + + companion object { + + /** Generous upper bound — the handler normally answers in well under a millisecond. */ + private const val HANDLER_TIMEOUT_MS = 2_000L } } From d32e00f72ba3bb1458c764e329b9877e4540c15f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 17:01:37 +0200 Subject: [PATCH 090/146] ui, constraints, sync: listen with Flow instead of Rx Observable Converts the last four non-pump rxBus.toObservable subscriptions. QuickWizardManagementViewModel and BgQualityCheckPlugin are plain one-liners: collect on viewModelScope resp. an own IO scope cancelled in onStop, with CoroutineStart.UNDISPATCHED because RxBus has no replay. DataHandlerMobile needed a new operator. Its heart-rate and steps handlers batch Wear reconnect bursts with the RxJava idiom publish { shared -> shared.buffer(shared.debounce(quietPeriod)) } so this adds Flow.chunkedOnQuietPeriod() next to collectResilient in :core:interfaces commonMain. It collects items and emits the batch once nothing arrived for the quiet period. transformLatest cancels and joins the previous block before starting the new one, so every item restarts the timer and the batch needs no lock. A batch still waiting is dropped when the collector is cancelled, which is what disposing the RxJava subscription did. Four tests on a virtual clock cover it: one burst gives one batch, a later burst gives a second batch with no carry over, a source that ends still delivers its last batch, and an idle source produces nothing. Dead AapsSchedulers, FabricPrivacy and CompositeDisposable members and their imports are removed where nothing else used them, with the matching test constructor calls. --- .../interfaces/rx/ChunkedOnQuietPeriodTest.kt | 90 +++++++++++++++++++ .../interfaces/rx/ChunkedOnQuietPeriod.kt | 39 ++++++++ .../bgQualityCheck/BgQualityCheckPlugin.kt | 28 +++--- .../BgQualityCheckPluginTest.kt | 4 +- .../wear/wearintegration/DataHandlerMobile.kt | 43 +++------ .../DataHandlerMobileUserActionTest.kt | 2 +- .../DataHandlerMobileWearBolusTest.kt | 2 +- .../QuickWizardManagementViewModel.kt | 25 ++---- .../QuickWizardManagementViewModelTest.kt | 10 +-- 9 files changed, 170 insertions(+), 73 deletions(-) create mode 100644 core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriodTest.kt create mode 100644 core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriod.kt diff --git a/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriodTest.kt b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriodTest.kt new file mode 100644 index 000000000000..86e3b6338917 --- /dev/null +++ b/core/interfaces/src/androidHostTest/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriodTest.kt @@ -0,0 +1,90 @@ +package app.aaps.core.interfaces.rx + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +/** + * Covers [chunkedOnQuietPeriod], the Flow replacement for the RxJava + * `publish { shared.buffer(shared.debounce(quietPeriod)) }` batching used for the Wear health + * events. The virtual clock of [runTest] makes the quiet period exact instead of flaky. + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class ChunkedOnQuietPeriodTest { + + private val quietPeriod = 1_000L + + @Test + fun `items sent in one burst come out as a single batch`() = runTest { + val source = MutableSharedFlow() + val batches = mutableListOf>() + val collector = launch { source.chunkedOnQuietPeriod(quietPeriod).collect { batches += it } } + runCurrent() + + source.emit(1) + advanceTimeBy(quietPeriod / 2) + source.emit(2) + advanceTimeBy(quietPeriod / 2) + source.emit(3) + + // Every item restarted the timer, so nothing is out yet even though 1.5 periods passed. + advanceTimeBy(quietPeriod - 1) + runCurrent() + assertThat(batches).isEmpty() + + advanceTimeBy(1) + runCurrent() + assertThat(batches).containsExactly(listOf(1, 2, 3)) + collector.cancel() + } + + @Test + fun `a new burst after the quiet period starts a new batch`() = runTest { + val source = MutableSharedFlow() + val batches = mutableListOf>() + val collector = launch { source.chunkedOnQuietPeriod(quietPeriod).collect { batches += it } } + runCurrent() + + source.emit(1) + advanceTimeBy(quietPeriod + 1) + runCurrent() + source.emit(2) + source.emit(3) + advanceTimeBy(quietPeriod + 1) + runCurrent() + + // Batches must not carry items over from the batch before them. + assertThat(batches).containsExactly(listOf(1), listOf(2, 3)).inOrder() + collector.cancel() + } + + @Test + fun `a source that ends still delivers the last batch`() = runTest { + // flowOf completes right away, so the batch is only released by the quiet period timer. + val batches = flowOf(1, 2, 3).chunkedOnQuietPeriod(quietPeriod).toList() + + assertThat(batches).containsExactly(listOf(1, 2, 3)) + } + + @Test + fun `a source that sends nothing produces no batch`() = runTest { + val source = MutableSharedFlow() + val batches = mutableListOf>() + val collector = launch { source.chunkedOnQuietPeriod(quietPeriod).collect { batches += it } } + runCurrent() + + advanceTimeBy(quietPeriod * 5) + runCurrent() + + // The timer only runs while a batch is open, so an idle source stays silent. + assertThat(batches).isEmpty() + collector.cancel() + } +} diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriod.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriod.kt new file mode 100644 index 000000000000..2b8fd274f9be --- /dev/null +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/ChunkedOnQuietPeriod.kt @@ -0,0 +1,39 @@ +package app.aaps.core.interfaces.rx + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.transformLatest + +/** + * Groups items into batches and emits a batch once nothing new arrived for [quietPeriodMs]. + * + * This is the Flow replacement for the RxJava idiom + * `publish { shared -> shared.buffer(shared.debounce(quietPeriod)) }`, which we used to coalesce + * bursts of events (for example the Wear Data Layer replaying its queue after a reconnect). The + * timer stays idle while nothing arrives, unlike a fixed window, so a single event is still + * delivered after one quiet period instead of waiting for a window boundary. + * + * `transformLatest` cancels and joins the previous block before it starts the new one, so the batch + * is only ever touched by one coroutine and needs no lock: every new item restarts the [delay], and + * the block reaches the emit only after a full quiet period. + * + * A batch that is waiting for its quiet period is dropped if the collector is cancelled. That is + * what the RxJava version did when its subscription was disposed, and it keeps items from being + * stored twice. + * + * @param quietPeriodMs how long the source has to stay silent before the batch is emitted + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun Flow.chunkedOnQuietPeriod(quietPeriodMs: Long): Flow> = flow { + // Built inside flow { } so every collection of the result gets its own batch. + val batch = mutableListOf() + transformLatest { item -> + batch += item + delay(quietPeriodMs) + val complete = batch.toList() + batch.clear() + emit(complete) + }.collect { emit(it) } +} diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt index 4b91ca5ff7c6..fd782e0e8e87 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt @@ -11,14 +11,16 @@ import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventBucketedDataCreated import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.plugins.constraints.R -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -34,9 +36,6 @@ class BgQualityCheckPlugin @Inject constructor( rh: ResourceHelper, private val rxBus: RxBus, private val iobCobCalculator: IobCobCalculator, - private val aapsSchedulers: - AapsSchedulers, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil ) : PluginBase( PluginDescription() @@ -47,19 +46,22 @@ class BgQualityCheckPlugin @Inject constructor( aapsLogger, rh ), PluginConstraints, BgQualityCheck { - private var disposable: CompositeDisposable = CompositeDisposable() + private var scope: CoroutineScope? = null override suspend fun onStart() { super.onStart() - disposable += rxBus - .toObservable(EventBucketedDataCreated::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ processBgData() }, fabricPrivacy::logException) + // Own scope on IO, matching observeOn(aapsSchedulers.io), cancelled in onStop like the + // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay: a scheduled + // collector could miss data created before it starts. + val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO).also { this.scope = it } + rxBus.toFlow(EventBucketedDataCreated::class.java) + .collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { processBgData() } } override suspend fun onStop() { super.onStop() - disposable.clear() + scope?.cancel() + scope = null } private val _stateFlow = MutableStateFlow(BgQualityCheck.State.UNKNOWN) diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPluginTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPluginTest.kt index 04178b8bc265..80445dde9295 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPluginTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPluginTest.kt @@ -10,7 +10,6 @@ import app.aaps.core.interfaces.bgQualityCheck.BgQualityCheck import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.fromGv import app.aaps.shared.tests.TestBase @@ -27,7 +26,6 @@ class BgQualityCheckPluginTest : TestBase() { @Mock lateinit var rh: ResourceHelper @Mock lateinit var iobCobCalculator: IobCobCalculator - @Mock lateinit var fabricPrivacy: FabricPrivacy @Mock lateinit var dateUtil: DateUtil @Mock lateinit var autosensDataStore: AutosensDataStore @@ -39,7 +37,7 @@ class BgQualityCheckPluginTest : TestBase() { @BeforeEach fun mock() { plugin = - BgQualityCheckPlugin(aapsLogger, rh, rxBus, iobCobCalculator, aapsSchedulers, fabricPrivacy, dateUtil) + BgQualityCheckPlugin(aapsLogger, rh, rxBus, iobCobCalculator, dateUtil) whenever(iobCobCalculator.ads).thenReturn(autosensDataStore) whenever(rh.gs(anyInt())).thenReturn("") whenever(rh.gs(anyInt(), any(), any())).thenReturn("") diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index eecdb5b7ab41..0b37673917e9 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -51,8 +51,8 @@ import app.aaps.core.interfaces.pump.PumpStatusProvider import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.receivers.ReceiverStatusStore import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.chunkedOnQuietPeriod import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventShowSnackbar @@ -98,19 +98,15 @@ import app.aaps.core.ui.clientcontrol.failText import app.aaps.core.ui.compose.DarkGeneralColors import app.aaps.core.ui.compose.LightGeneralColors import app.aaps.plugins.sync.R -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.rx3.rxCompletable import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.LinkedList import java.util.Locale -import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.math.abs @@ -122,7 +118,6 @@ private const val HEALTH_EVENT_QUIET_PERIOD_MS = 500L @Singleton class DataHandlerMobile @Inject constructor( - private val aapsSchedulers: AapsSchedulers, private val context: Context, private val rxBus: RxBus, private val aapsLogger: AAPSLogger, @@ -163,10 +158,9 @@ class DataHandlerMobile @Inject constructor( @Inject lateinit var automation: Automation @Inject lateinit var scenes: SceneAutomationApi @Inject lateinit var sceneActions: SceneActions - private val disposable = CompositeDisposable() - // App lifetime, matching the disposable above: this is a @Singleton that subscribes in init and - // never tears down. Dispatchers.IO because that is what aapsSchedulers.io gave these handlers, and + // App lifetime: this is a @Singleton that subscribes in init and + // never tears down. Dispatchers.IO because that is what the io scheduler gave these handlers, and // they do database and broadcast work - the Default pool would be the wrong one. private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -343,27 +337,16 @@ class DataHandlerMobile @Inject constructor( onEventSync { uiInteraction.stopAlarm("Muted from wear") } onEventSync { fabricPrivacy.logWearException(it) } // Coalesce Wear reconnect-flush bursts (Data Layer replays queued events back-to-back). - // publish/debounce keeps the timer idle when no events arrive, unlike fixed-window buffer(). - disposable += rxBus - .toObservable(EventData.ActionHeartRate::class.java) - .publish { shared -> shared.buffer(shared.debounce(HEALTH_EVENT_QUIET_PERIOD_MS, TimeUnit.MILLISECONDS, aapsSchedulers.io)) } - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { handleHeartRateBatch(it) } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() - } - .subscribe() - disposable += rxBus - .toObservable(EventData.ActionStepsRate::class.java) - .publish { shared -> shared.buffer(shared.debounce(HEALTH_EVENT_QUIET_PERIOD_MS, TimeUnit.MILLISECONDS, aapsSchedulers.io)) } - .observeOn(aapsSchedulers.io) - .concatMapCompletable { - rxCompletable { handleStepsCountBatch(it) } - .doOnError(fabricPrivacy::logException) - .onErrorComplete() - } - .subscribe() + // chunkedOnQuietPeriod keeps the timer idle when no events arrive, unlike a fixed window. + // The collector is sequential, so batches are still handled one after another the way + // concatMapCompletable did, and collectResilient logs and continues like doOnError + + // onErrorComplete. + rxBus.toFlow(EventData.ActionHeartRate::class.java) + .chunkedOnQuietPeriod(HEALTH_EVENT_QUIET_PERIOD_MS) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { handleHeartRateBatch(it) } + rxBus.toFlow(EventData.ActionStepsRate::class.java) + .chunkedOnQuietPeriod(HEALTH_EVENT_QUIET_PERIOD_MS) + .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { handleStepsCountBatch(it) } onEventSync(detail = { " watchface=${it.customWatchface}" }) { handleGetCustomWatchface(it) } } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileUserActionTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileUserActionTest.kt index ed771e869fc5..ed3f44d6c090 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileUserActionTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileUserActionTest.kt @@ -71,7 +71,7 @@ class DataHandlerMobileUserActionTest : TestBaseWithProfile() { @BeforeEach fun prepare() { sut = DataHandlerMobile( - aapsSchedulers, context, rxBus, aapsLogger, rh, preferences, config, + context, rxBus, aapsLogger, rh, preferences, config, iobCobCalculator, processedTbrEbData, smbGlucoseStatusProvider, profileFunction, profileUtil, loop, processedDeviceStatusData, receiverStatusStore, quickWizard, trendCalculator, dateUtil, constraintsChecker, activePlugin, commandQueue, fabricPrivacy, uiInteraction, diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt index 87e22303324b..ba09ea44e4f9 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt @@ -93,7 +93,7 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { @BeforeEach fun prepare() { sut = DataHandlerMobile( - aapsSchedulers, context, rxBus, aapsLogger, rh, preferences, config, + context, rxBus, aapsLogger, rh, preferences, config, iobCobCalculator, processedTbrEbData, smbGlucoseStatusProvider, profileFunction, profileUtil, loop, processedDeviceStatusData, receiverStatusStore, quickWizard, trendCalculator, dateUtil, constraintsChecker, activePlugin, commandQueue, fabricPrivacy, uiInteraction, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt index c1e80030794b..677206a1764f 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt @@ -7,8 +7,8 @@ import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.BooleanKey @@ -19,8 +19,7 @@ import app.aaps.core.objects.wizard.QuickWizardMode import app.aaps.core.ui.compose.ScreenMode import app.aaps.ui.events.EventQuickWizardChange import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow @@ -46,7 +45,6 @@ import javax.inject.Inject class QuickWizardManagementViewModel @Inject constructor( private val quickWizard: QuickWizard, private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, private val constraintChecker: ConstraintsChecker, private val preferences: Preferences, val rh: ResourceHelper, @@ -54,7 +52,6 @@ class QuickWizardManagementViewModel @Inject constructor( private val aapsLogger: AAPSLogger ) : ViewModel() { - private val disposable = CompositeDisposable() private val _uiState = MutableStateFlow(QuickWizardManagementUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -86,11 +83,6 @@ class QuickWizardManagementViewModel @Inject constructor( observeQuickWizardPrefChanges() } - override fun onCleared() { - super.onCleared() - disposable.clear() - } - /** * Load QuickWizard entries and preferences */ @@ -130,14 +122,11 @@ class QuickWizardManagementViewModel @Inject constructor( * Observe QuickWizard changes from RxBus */ private fun observeQuickWizardChanges() { - disposable += rxBus - .toObservable(EventQuickWizardChange::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ - loadData() - }, { throwable -> - aapsLogger.error(LTag.UI, "Error observing QuickWizard changes", throwable) - }) + // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model + // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay: a scheduled + // collector could miss a change sent before it starts. + rxBus.toFlow(EventQuickWizardChange::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.UI, start = CoroutineStart.UNDISPATCHED) { loadData() } } /** diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt index ca95f0984857..12739de198ad 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt @@ -3,7 +3,6 @@ package app.aaps.ui.compose.quickWizard.viewmodels import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.interfaces.Preferences @@ -13,11 +12,10 @@ import app.aaps.core.objects.wizard.QuickWizardMode import app.aaps.core.ui.compose.ScreenMode import app.aaps.ui.events.EventQuickWizardChange import com.google.common.truth.Truth.assertThat -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -41,7 +39,6 @@ internal class QuickWizardManagementViewModelTest { @Mock private lateinit var quickWizard: QuickWizard @Mock private lateinit var rxBus: RxBus - @Mock private lateinit var aapsSchedulers: AapsSchedulers @Mock private lateinit var constraintChecker: ConstraintsChecker @Mock private lateinit var preferences: Preferences @Mock private lateinit var rh: ResourceHelper @@ -58,10 +55,9 @@ internal class QuickWizardManagementViewModelTest { Dispatchers.setMain(StandardTestDispatcher()) // Synchronous init wiring that must return non-null flows/streams to avoid construction NPEs. whenever(quickWizard.changes).thenReturn(MutableStateFlow(0)) - whenever(rxBus.toObservable(EventQuickWizardChange::class.java)).thenReturn(Observable.empty()) - whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) + whenever(rxBus.toFlow(EventQuickWizardChange::class.java)).thenReturn(emptyFlow()) sut = QuickWizardManagementViewModel( - quickWizard, rxBus, aapsSchedulers, constraintChecker, preferences, rh, dateUtil, aapsLogger + quickWizard, rxBus, constraintChecker, preferences, rh, dateUtil, aapsLogger ) } From 09667d62ffcb84ae18675e5a83a8855163552961 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 17:16:51 +0200 Subject: [PATCH 091/146] Dana pumps: listen with Flow instead of Rx Observable Converts the 14 rxBus.toObservable subscriptions in :pump:danar, :pump:dana and :pump:danars. Plugins collect on a scope created in onStart and cancelled in onStop, which is where the CompositeDisposable was cleared. AbstractDanaRPlugin, DanaRPlugin and DanaRKoreanPlugin already had such a scope for a preference observer and reuse it. DanaRv2Plugin and DanaRSPlugin get their own. The two execution services collect on a service lifetime scope cancelled in onDestroy. They must not use the injected appScope: that is the application scope and outlives the service. The Dana view models collect on viewModelScope, which dies with the view model the way the CompositeDisposable did. All of them use CoroutineStart.UNDISPATCHED, because RxBus has no replay: a scheduled collector could miss an event sent before it starts, while subscribe() used to register right away. DanaHistoryViewModel keeps its CompositeDisposable - it also observes the Room history DAO, which is a separate RxJava source and not part of this change. Dead AapsSchedulers, FabricPrivacy and CompositeDisposable members and their imports are removed where nothing else used them, together with the matching test constructor calls and field assignments. The Dana view model tests now stub toFlow instead of toObservable. --- .../constraints/ConstraintsCheckerImplTest.kt | 6 +- .../pump/dana/compose/DanaHistoryViewModel.kt | 16 +++--- .../dana/compose/DanaOverviewViewModel.kt | 23 +++----- .../dana/compose/DanaHistoryViewModelTest.kt | 3 +- .../dana/compose/DanaOverviewViewModelTest.kt | 6 +- .../aaps/pump/danar/AbstractDanaRPlugin.kt | 19 +++---- .../kotlin/app/aaps/pump/danar/DanaRPlugin.kt | 18 +++--- .../services/AbstractDanaRExecutionService.kt | 55 +++++++++---------- .../pump/danarkorean/DanaRKoreanPlugin.kt | 18 +++--- .../app/aaps/pump/danarv2/DanaRv2Plugin.kt | 29 ++++++---- .../app/aaps/pump/danar/DanaRPluginTest.kt | 2 +- .../AbstractDanaRExecutionServiceTest.kt | 2 - .../services/DanaRExecutionServiceTest.kt | 2 - .../pump/danarkorean/DanaRKoreanPluginTest.kt | 2 +- .../DanaRKoreanExecutionServiceTest.kt | 2 - .../aaps/pump/danarv2/DanaRv2PluginTest.kt | 4 +- .../services/DanaRv2ExecutionServiceTest.kt | 2 - .../emulator/DanaRSServiceIntegrationTest.kt | 2 - .../app/aaps/pump/danars/DanaRSPlugin.kt | 35 ++++++------ .../danars/compose/DanaRSOverviewViewModel.kt | 3 - .../pump/danars/services/DanaRSService.kt | 25 +++++---- .../app/aaps/pump/danars/DanaRSPluginTest.kt | 4 +- ...naRsPacketNotifyDeliveryRateDisplayTest.kt | 2 - .../compose/DanaRSOverviewViewModelTest.kt | 6 +- .../pump/danars/services/DanaRSServiceTest.kt | 2 - 25 files changed, 134 insertions(+), 154 deletions(-) diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt index 1ede51726dff..8309343333b4 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt @@ -162,14 +162,14 @@ class ConstraintsCheckerImplTest : TestBaseWithProfile() { objectivesPlugin = ObjectivesPlugin(aapsLogger, rh, preferences, config, objectives) runBlocking { objectivesPlugin.onStart() } danaRPlugin = DanaRPlugin( - aapsLogger, rh, preferences, config, commandQueue, aapsSchedulers, rxBus, context, activePlugin, danaPump, dateUtil, fabricPrivacy, pumpSync, + aapsLogger, rh, preferences, config, commandQueue, rxBus, context, activePlugin, danaPump, dateUtil, pumpSync, notificationManager, danaHistoryDatabase, decimalFormatter, bolusProgressData, pumpEnactResultProvider ) danaRSPlugin = DanaRSPlugin( - aapsLogger, rh, preferences, commandQueue, aapsSchedulers, rxBus, context, + aapsLogger, rh, preferences, commandQueue, rxBus, context, danaPump, detailedBolusInfoStorage, temporaryBasalStorage, - fabricPrivacy, dateUtil, danaHistoryDatabase, decimalFormatter, pumpEnactResultProvider, blePreCheck, bolusProgressData + dateUtil, danaHistoryDatabase, decimalFormatter, pumpEnactResultProvider, blePreCheck, bolusProgressData ) insightPlugin = InsightPlugin( aapsLogger, rh, preferences, commandQueue, rxBus, diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt index 49308da008fc..23e997908120 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt @@ -14,6 +14,7 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.ui.compose.pump.PumpHistoryType @@ -26,6 +27,7 @@ import app.aaps.pump.dana.events.EventDanaRSyncStatus import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -74,13 +76,13 @@ class DanaHistoryViewModel @Inject constructor( _uiState.value = PumpHistoryUiState(availableTypes = types, selectedType = types.firstOrNull()) - // Listen for sync status - disposable += rxBus - .toObservable(EventDanaRSyncStatus::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ event -> - _uiState.update { it.copy(statusMessage = event.message) } - }, { aapsLogger.error(LTag.PUMP, "Error", it) }) + // Listen for sync status. viewModelScope is Main, like observeOn(aapsSchedulers.main), and + // dies with the view model like the CompositeDisposable did. UNDISPATCHED because RxBus has + // no replay, so a scheduled collector could miss a status sent before it starts. + rxBus.toFlow(EventDanaRSyncStatus::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> + _uiState.update { it.copy(statusMessage = event.message) } + } // Load initial data types.firstOrNull()?.let { loadRecords(it.type) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt index 7cc6cefd1aba..f97e0dfdfea4 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt @@ -24,8 +24,8 @@ import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.pump.PumpRate import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.interfaces.Preferences @@ -43,8 +43,7 @@ import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.dana.keys.DanaStringNonKey import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -73,7 +72,6 @@ open class DanaOverviewViewModel @Inject constructor( private val aapsLogger: AAPSLogger, protected val rh: ResourceHelper, rxBus: RxBus, - aapsSchedulers: AapsSchedulers, private val commandQueue: CommandQueue, private val dateUtil: DateUtil, private val danaPump: DanaPump, @@ -85,7 +83,6 @@ open class DanaOverviewViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { - private val disposable = CompositeDisposable() private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events @@ -96,14 +93,13 @@ open class DanaOverviewViewModel @Inject constructor( protected val rxTrigger = MutableStateFlow(0L) init { - disposable += rxBus - .toObservable(EventDanaRNewStatus::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ rxTrigger.value = System.currentTimeMillis() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) - disposable += rxBus - .toObservable(EventInitializationChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ rxTrigger.value = System.currentTimeMillis() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) + // viewModelScope dies with the view model like the CompositeDisposable did. The bodies only + // write a timestamp, so the dispatcher does not matter. UNDISPATCHED because RxBus has no + // replay, so a scheduled collector could miss an event sent before it starts. + rxBus.toFlow(EventDanaRNewStatus::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } + rxBus.toFlow(EventInitializationChanged::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } // Observe EB/TB database changes for immediate UI updates persistenceLayer.observeChanges(EB::class.java) @@ -148,7 +144,6 @@ open class DanaOverviewViewModel @Inject constructor( override fun onCleared() { super.onCleared() - disposable.clear() } fun onRefreshClick() { diff --git a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt index c1e8897d8ba4..19ba04fdf4e1 100644 --- a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt +++ b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -77,7 +78,7 @@ internal class DanaHistoryViewModelTest { whenever(rh.gs(anyInt())).thenReturn("") // rx wiring touched at construction - whenever(rxBus.toObservable(EventDanaRSyncStatus::class.java)).thenReturn(Observable.empty()) + whenever(rxBus.toFlow(EventDanaRSyncStatus::class.java)).thenReturn(emptyFlow()) whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) diff --git a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt index 1fed187c80b4..e7834e48a9d1 100644 --- a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt +++ b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt @@ -87,8 +87,8 @@ internal class DanaOverviewViewModelTest { // rx wiring touched at construction whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toObservable(EventDanaRNewStatus::class.java)).thenReturn(Observable.empty()) - whenever(rxBus.toObservable(EventInitializationChanged::class.java)).thenReturn(Observable.empty()) + whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) @@ -121,7 +121,7 @@ internal class DanaOverviewViewModelTest { } private fun createViewModel() = DanaOverviewViewModel( - aapsLogger, rh, rxBus, aapsSchedulers, commandQueue, dateUtil, danaPump, + aapsLogger, rh, rxBus, commandQueue, dateUtil, danaPump, activePlugin, ch, persistenceLayer, uel, preferences, context ) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt index f09527145863..49ae129a9f17 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt @@ -25,8 +25,8 @@ import app.aaps.core.interfaces.pump.PumpSync.TemporaryBasalType import app.aaps.core.interfaces.pump.mapState import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventConfigBuilderChange import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter @@ -43,9 +43,8 @@ import app.aaps.pump.dana.keys.DanaIntentKey import app.aaps.pump.dana.keys.DanaStringNonKey import app.aaps.pump.danar.compose.DanaRComposeContent import app.aaps.pump.danar.services.AbstractDanaRExecutionService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -67,7 +66,6 @@ abstract class AbstractDanaRPlugin protected constructor( preferences: Preferences, protected val config: Config, commandQueue: CommandQueue, - protected var aapsSchedulers: AapsSchedulers, protected var rxBus: RxBus, protected var activePlugin: ActivePlugin, protected var dateUtil: DateUtil, @@ -94,20 +92,20 @@ abstract class AbstractDanaRPlugin protected constructor( ), Pump, Dana, PumpPluginConstraints, OwnDatabasePlugin { protected var executionService: AbstractDanaRExecutionService? = null - protected var disposable = CompositeDisposable() private var scope: CoroutineScope? = null override var pumpDescription = PumpDescription() protected set override suspend fun onStart() { super.onStart() - disposable += rxBus - .toObservable(EventConfigBuilderChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { danaPump.reset() } - val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope + // Same scope as the preference observer below: IO, like the io scheduler used before, and + // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus + // has no replay, so a scheduled collector could miss a change sent before it starts. + rxBus.toFlow(EventConfigBuilderChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { danaPump.reset() } + preferences.observe(DanaStringNonKey.RName).drop(1).onEach { danaPump.reset() pumpSync.connectNewPump(true) @@ -120,7 +118,6 @@ abstract class AbstractDanaRPlugin protected constructor( super.onStop() scope?.cancel() scope = null - disposable.clear() } override fun isSuspended(): Boolean { diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt index 5cbd7d9633d3..58a843895804 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt @@ -20,15 +20,14 @@ import app.aaps.core.interfaces.pump.PumpSync.TemporaryBasalType import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Round.ceilTo import app.aaps.core.interfaces.utils.Round.floorTo import app.aaps.core.interfaces.utils.Round.roundTo -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -37,8 +36,8 @@ import app.aaps.pump.dana.database.DanaHistoryDatabase import app.aaps.pump.dana.keys.DanaBooleanKey import app.aaps.pump.dana.keys.DanaIntKey import app.aaps.pump.danar.services.DanaRExecutionService -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -58,13 +57,11 @@ class DanaRPlugin @Inject constructor( preferences: Preferences, config: Config, commandQueue: CommandQueue, - aapsSchedulers: AapsSchedulers, rxBus: RxBus, private val context: Context, activePlugin: ActivePlugin, danaPump: DanaPump, dateUtil: DateUtil, - private val fabricPrivacy: FabricPrivacy, pumpSync: PumpSync, notificationManager: NotificationManager, danaHistoryDatabase: DanaHistoryDatabase, @@ -78,7 +75,6 @@ class DanaRPlugin @Inject constructor( preferences, config, commandQueue, - aapsSchedulers, rxBus, activePlugin, dateUtil, @@ -118,10 +114,11 @@ class DanaRPlugin @Inject constructor( executionService?.extendedBolusStop() } }.launchIn(newScope) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + // Same scope as the preference observer above: IO, like the io scheduler used before, and + // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus + // has no replay, so a scheduled collector could miss an exit sent before it starts. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } @@ -129,7 +126,6 @@ class DanaRPlugin @Inject constructor( scope?.cancel() scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt index a67b316e5130..da7c031f33ec 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt @@ -19,14 +19,13 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.rfcomm.RfcommSocket import app.aaps.core.interfaces.pump.rfcomm.RfcommTransport import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventBTChange import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.pump.dana.DanaPump import app.aaps.pump.dana.R @@ -49,9 +48,11 @@ import app.aaps.pump.danar.comm.MsgPCCommStart import app.aaps.pump.danar.comm.MsgPCCommStop import dagger.android.DaggerService import dagger.android.HasAndroidInjector -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.cancel +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.Dispatchers import java.io.IOException import javax.inject.Inject import javax.inject.Provider @@ -70,9 +71,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { @Inject lateinit var context: Context @Inject lateinit var rh: ResourceHelper @Inject lateinit var danaPump: DanaPump - @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var dateUtil: DateUtil - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var pumpSync: PumpSync @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var notificationManager: NotificationManager @@ -81,7 +80,8 @@ abstract class AbstractDanaRExecutionService : DaggerService() { @Inject lateinit var bolusProgressData: BolusProgressData @Inject @ApplicationScope lateinit var appScope: CoroutineScope - private val disposable = CompositeDisposable() + // Service lifetime. appScope above is the application scope and must not be cancelled here. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // These are read/written across the connect() worker thread, the reader thread, the BT/app-exit // observers (io scheduler), disconnect(), and the command methods - @Volatile for visibility. @Volatile protected var mRfcommSocket: RfcommSocket? = null @@ -107,30 +107,29 @@ abstract class AbstractDanaRExecutionService : DaggerService() { override fun onCreate() { super.onCreate() - disposable += rxBus - .toObservable(EventBTChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ event: EventBTChange -> - if (event.state === EventBTChange.Change.DISCONNECT) { - aapsLogger.debug(LTag.PUMP, "Device was disconnected " + event.deviceName) //Device was disconnected - if (preferences.get(DanaStringNonKey.RName) == event.deviceName) { - mSerialIOThread?.disconnect("BT disconnection broadcast") - rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) - } - } - }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.debug(LTag.PUMP, "EventAppExit received") - mSerialIOThread?.disconnect("Application exit") - stopSelf() - }, fabricPrivacy::logException) + // Service lifetime scope on IO, like the io scheduler used before, cancelled in onDestroy + // like the CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a + // scheduled collector could miss an event sent before it starts. + rxBus.toFlow(EventBTChange::class.java) + .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> + if (event.state === EventBTChange.Change.DISCONNECT) { + aapsLogger.debug(LTag.PUMP, "Device was disconnected " + event.deviceName) //Device was disconnected + if (preferences.get(DanaStringNonKey.RName) == event.deviceName) { + mSerialIOThread?.disconnect("BT disconnection broadcast") + rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED)) + } + } + } + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.debug(LTag.PUMP, "EventAppExit received") + mSerialIOThread?.disconnect("Application exit") + stopSelf() + } } override fun onDestroy() { - disposable.clear() + scope.cancel() super.onDestroy() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt index b59cfa69e530..aa7885a63927 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt @@ -20,13 +20,12 @@ import app.aaps.core.interfaces.pump.PumpSync.TemporaryBasalType import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Round -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -35,8 +34,8 @@ import app.aaps.pump.dana.database.DanaHistoryDatabase import app.aaps.pump.dana.keys.DanaBooleanKey import app.aaps.pump.danar.AbstractDanaRPlugin import app.aaps.pump.danarkorean.services.DanaRKoreanExecutionService -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -52,7 +51,6 @@ import kotlin.math.max @Singleton class DanaRKoreanPlugin @Inject constructor( aapsLogger: AAPSLogger, - aapsSchedulers: AapsSchedulers, rxBus: RxBus, private val context: Context, rh: ResourceHelper, @@ -60,7 +58,6 @@ class DanaRKoreanPlugin @Inject constructor( commandQueue: CommandQueue, danaPump: DanaPump, dateUtil: DateUtil, - private val fabricPrivacy: FabricPrivacy, pumpSync: PumpSync, preferences: Preferences, config: Config, @@ -76,7 +73,6 @@ class DanaRKoreanPlugin @Inject constructor( preferences, config, commandQueue, - aapsSchedulers, rxBus, activePlugin, dateUtil, @@ -103,10 +99,11 @@ class DanaRKoreanPlugin @Inject constructor( executionService?.extendedBolusStop() } }.launchIn(newScope) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + // Same scope as the preference observer above: IO, like the io scheduler used before, and + // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus + // has no replay, so a scheduled collector could miss an exit sent before it starts. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } @@ -114,7 +111,6 @@ class DanaRKoreanPlugin @Inject constructor( scope?.cancel() scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt index 57a20d273f92..b2195ffe17be 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt @@ -23,15 +23,14 @@ import app.aaps.core.interfaces.pump.TemporaryBasalStorage import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Round.ceilTo import app.aaps.core.interfaces.utils.Round.floorTo import app.aaps.core.interfaces.utils.Round.roundTo -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef import app.aaps.pump.dana.DanaPump @@ -41,7 +40,11 @@ import app.aaps.pump.dana.keys.DanaBooleanKey import app.aaps.pump.dana.keys.DanaIntKey import app.aaps.pump.danar.AbstractDanaRPlugin import app.aaps.pump.danarv2.services.DanaRv2ExecutionService -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -51,7 +54,6 @@ import kotlin.math.max @Singleton class DanaRv2Plugin @Inject constructor( aapsLogger: AAPSLogger, - aapsSchedulers: AapsSchedulers, rxBus: RxBus, private val context: Context, rh: ResourceHelper, @@ -61,7 +63,6 @@ class DanaRv2Plugin @Inject constructor( private val detailedBolusInfoStorage: DetailedBolusInfoStorage, private val temporaryBasalStorage: TemporaryBasalStorage, dateUtil: DateUtil, - private val fabricPrivacy: FabricPrivacy, pumpSync: PumpSync, preferences: Preferences, config: Config, @@ -77,7 +78,6 @@ class DanaRv2Plugin @Inject constructor( preferences, config, commandQueue, - aapsSchedulers, rxBus, activePlugin, dateUtil, @@ -101,6 +101,9 @@ class DanaRv2Plugin @Inject constructor( } } + // The parent keeps its own private scope, so this one only covers what this plugin starts. + private var scope: CoroutineScope? = null + init { pluginDescription.description(R.string.description_pump_dana_r_v2) pumpDescription.fillFor(PumpType.DANA_RV2) @@ -109,16 +112,20 @@ class DanaRv2Plugin @Inject constructor( override suspend fun onStart() { val intent = Intent(context, DanaRv2ExecutionService::class.java) context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + // Own scope on IO, like the io scheduler used before, cancelled in onStop like the + // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss an exit sent before it starts. + val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } override suspend fun onStop() { + scope?.cancel() + scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt index 046c3c74d2d9..f9a2e74265a1 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/DanaRPluginTest.kt @@ -37,7 +37,7 @@ class DanaRPluginTest : TestBaseWithProfile() { whenever(rh.gs(app.aaps.core.ui.R.string.limitingpercentrate)).thenReturn("Limiting max percent rate to %1\$d%% because of %2\$s") danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRPlugin = DanaRPlugin( - aapsLogger, rh, preferences, config, commandQueue, aapsSchedulers, rxBus, context, activePlugin, danaPump, dateUtil, fabricPrivacy, pumpSync, + aapsLogger, rh, preferences, config, commandQueue, rxBus, context, activePlugin, danaPump, dateUtil, pumpSync, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt index d3d55eaf658c..6c5dcfad1fc7 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionServiceTest.kt @@ -75,9 +75,7 @@ class AbstractDanaRExecutionServiceTest : TestBaseWithProfile() { testService.context = context testService.rh = rh testService.danaPump = danaPump - testService.fabricPrivacy = fabricPrivacy testService.dateUtil = dateUtil - testService.aapsSchedulers = aapsSchedulers testService.pumpSync = pumpSync testService.activePlugin = activePlugin testService.notificationManager = notificationManager diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/DanaRExecutionServiceTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/DanaRExecutionServiceTest.kt index aefcaaa89540..66cae6c55879 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/DanaRExecutionServiceTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/services/DanaRExecutionServiceTest.kt @@ -39,9 +39,7 @@ class DanaRExecutionServiceTest : TestBaseWithProfile() { danaRExecutionService.context = context danaRExecutionService.rh = rh danaRExecutionService.danaPump = danaPump - danaRExecutionService.fabricPrivacy = fabricPrivacy danaRExecutionService.dateUtil = dateUtil - danaRExecutionService.aapsSchedulers = aapsSchedulers danaRExecutionService.pumpSync = pumpSync danaRExecutionService.activePlugin = activePlugin danaRExecutionService.notificationManager = notificationManager diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt index bb481a294a12..e2ba77c46d51 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPluginTest.kt @@ -37,7 +37,7 @@ class DanaRKoreanPluginTest : TestBaseWithProfile() { whenever(rh.gs(app.aaps.core.ui.R.string.limitingpercentrate)).thenReturn("Limiting max percent rate to %1\$d%% because of %2\$s") danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRPlugin = DanaRKoreanPlugin( - aapsLogger, aapsSchedulers, rxBus, context, rh, activePlugin, commandQueue, danaPump, dateUtil, fabricPrivacy, + aapsLogger, rxBus, context, rh, activePlugin, commandQueue, danaPump, dateUtil, pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionServiceTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionServiceTest.kt index 38c2dcb6cd78..ca0bcbe91572 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionServiceTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionServiceTest.kt @@ -40,9 +40,7 @@ class DanaRKoreanExecutionServiceTest : TestBaseWithProfile() { danaRKoreanExecutionService.context = context danaRKoreanExecutionService.rh = rh danaRKoreanExecutionService.danaPump = danaPump - danaRKoreanExecutionService.fabricPrivacy = fabricPrivacy danaRKoreanExecutionService.dateUtil = dateUtil - danaRKoreanExecutionService.aapsSchedulers = aapsSchedulers danaRKoreanExecutionService.pumpSync = pumpSync danaRKoreanExecutionService.activePlugin = activePlugin danaRKoreanExecutionService.notificationManager = notificationManager diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt index a313a7ee4d36..82294814d93c 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/DanaRv2PluginTest.kt @@ -41,8 +41,8 @@ class DanaRv2PluginTest : TestBaseWithProfile() { whenever(rh.gs(app.aaps.core.ui.R.string.limitingpercentrate)).thenReturn("Limiting max percent rate to %1\$d%% because of %2\$s") danaPump = DanaPump(aapsLogger, preferences, dateUtil, decimalFormatter, profileStoreProvider) danaRv2Plugin = DanaRv2Plugin( - aapsLogger, aapsSchedulers, rxBus, context, rh, activePlugin, commandQueue, danaPump, detailedBolusInfoStorage, - temporaryBasalStorage, dateUtil, fabricPrivacy, pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider + aapsLogger, rxBus, context, rh, activePlugin, commandQueue, danaPump, detailedBolusInfoStorage, + temporaryBasalStorage, dateUtil, pumpSync, preferences, config, notificationManager, danaHistoryDatabase, decimalFormatter, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), pumpEnactResultProvider ) } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionServiceTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionServiceTest.kt index 1358aed8432b..ee29c598f51b 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionServiceTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionServiceTest.kt @@ -42,9 +42,7 @@ class DanaRv2ExecutionServiceTest : TestBaseWithProfile() { danaRv2ExecutionService.context = context danaRv2ExecutionService.rh = rh danaRv2ExecutionService.danaPump = danaPump - danaRv2ExecutionService.fabricPrivacy = fabricPrivacy danaRv2ExecutionService.dateUtil = dateUtil - danaRv2ExecutionService.aapsSchedulers = aapsSchedulers danaRv2ExecutionService.pumpSync = pumpSync danaRv2ExecutionService.activePlugin = activePlugin danaRv2ExecutionService.uiInteraction = uiInteraction diff --git a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt index c6a4156da869..2c217883acfb 100644 --- a/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt +++ b/pump/danars-emulator/src/test/kotlin/app/aaps/pump/danars/emulator/DanaRSServiceIntegrationTest.kt @@ -202,7 +202,6 @@ class DanaRSServiceIntegrationTest : TestBase() { // Create service and wire all dependencies danaRSService = DanaRSService() danaRSService.aapsLogger = aapsLogger - danaRSService.aapsSchedulers = aapsSchedulers danaRSService.rxBus = rxBus danaRSService.preferences = preferences danaRSService.rh = rh @@ -213,7 +212,6 @@ class DanaRSServiceIntegrationTest : TestBase() { danaRSService.activePlugin = activePlugin danaRSService.uiInteraction = uiInteraction danaRSService.bleComm = bleComm - danaRSService.fabricPrivacy = fabricPrivacy danaRSService.pumpSync = pumpSync danaRSService.dateUtil = dateUtil danaRSService.bolusProgressData = bolusProgressData diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt index accf13831cbd..4586d2aa1637 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt @@ -32,14 +32,13 @@ import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.pump.mapState import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventConfigBuilderChange import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Round -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.icons.IcPluginDanaI import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -54,8 +53,11 @@ import app.aaps.pump.dana.keys.DanaStringComposedKey import app.aaps.pump.dana.keys.DanaStringNonKey import app.aaps.pump.danars.compose.DanaRSComposeContent import app.aaps.pump.danars.services.DanaRSService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -70,13 +72,11 @@ class DanaRSPlugin @Inject constructor( rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, - private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, private val context: Context, private val danaPump: DanaPump, private val detailedBolusInfoStorage: DetailedBolusInfoStorage, private val temporaryBasalStorage: TemporaryBasalStorage, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, private val danaHistoryDatabase: DanaHistoryDatabase, private val decimalFormatter: DecimalFormatter, @@ -103,7 +103,6 @@ class DanaRSPlugin @Inject constructor( aapsLogger, rh, preferences, commandQueue ), Pump, Dana, PumpPluginConstraints, OwnDatabasePlugin { - private val disposable = CompositeDisposable() private var danaRSService: DanaRSService? = null private var mDeviceAddress = "" var mDeviceName = "" @@ -115,6 +114,8 @@ class DanaRSPlugin @Inject constructor( override val lastBolusAmount: StateFlow = danaPump.lastBolusAmountFlow.mapState { it?.let(::PumpInsulin) } override val reservoirLevel: StateFlow = danaPump.reservoirRemainingUnitsFlow.mapState(::PumpInsulin) + private var scope: CoroutineScope? = null + override val pumpDescription get() = PumpDescription().fillFor(danaPump.pumpType()) @@ -122,20 +123,22 @@ class DanaRSPlugin @Inject constructor( super.onStart() val intent = Intent(context, DanaRSService::class.java) context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventConfigBuilderChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { danaPump.reset() } + // Own scope on IO, like the io scheduler used before, cancelled in onStop like the + // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss an event sent before it starts. + val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } + rxBus.toFlow(EventConfigBuilderChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { danaPump.reset() } changePump() // load device name } override suspend fun onStop() { + scope?.cancel() + scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModel.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModel.kt index 065bb3853783..2380c87fae8e 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModel.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModel.kt @@ -17,7 +17,6 @@ import app.aaps.core.interfaces.pump.ble.PairingState import app.aaps.core.interfaces.pump.ble.PairingStep import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.interfaces.Preferences @@ -40,7 +39,6 @@ class DanaRSOverviewViewModel @Inject constructor( aapsLogger: AAPSLogger, rh: ResourceHelper, rxBus: RxBus, - aapsSchedulers: AapsSchedulers, commandQueue: CommandQueue, dateUtil: DateUtil, private val danaPump: DanaPump, @@ -56,7 +54,6 @@ class DanaRSOverviewViewModel @Inject constructor( aapsLogger = aapsLogger, rh = rh, rxBus = rxBus, - aapsSchedulers = aapsSchedulers, commandQueue = commandQueue, dateUtil = dateUtil, danaPump = danaPump, diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt index 76ff917b8edb..3038f6cb2052 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt @@ -23,15 +23,14 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.dana.DanaPump @@ -82,9 +81,11 @@ import app.aaps.pump.danars.comm.DanaRSPacketOptionSetPumpTime import app.aaps.pump.danars.comm.DanaRSPacketOptionSetPumpUTCAndTimeZone import app.aaps.pump.danars.comm.DanaRSPacketOptionSetUserOption import dagger.android.DaggerService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -97,7 +98,6 @@ import kotlin.time.Duration.Companion.milliseconds class DanaRSService : DaggerService() { @Inject lateinit var aapsLogger: AAPSLogger - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var rxBus: RxBus @Inject lateinit var preferences: Preferences @Inject lateinit var rh: ResourceHelper @@ -109,7 +109,6 @@ class DanaRSService : DaggerService() { @Inject lateinit var uiInteraction: UiInteraction @Inject lateinit var notificationManager: NotificationManager @Inject lateinit var bleComm: BLEComm - @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var pumpSync: PumpSync @Inject lateinit var dateUtil: DateUtil @Inject lateinit var bolusProgressData: BolusProgressData @@ -155,20 +154,22 @@ class DanaRSService : DaggerService() { @Inject lateinit var danaRSPacketHistoryRefill: Provider @Inject lateinit var danaRSPacketHistorySuspend: Provider - private val disposable = CompositeDisposable() + // Service lifetime. appScope above is the application scope and must not be cancelled here. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val mBinder: IBinder = LocalBinder() private var lastApproachingDailyLimit: Long = 0 override fun onCreate() { super.onCreate() - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ stopSelf() }, fabricPrivacy::logException) + // IO like the io scheduler used before, cancelled in onDestroy like the CompositeDisposable + // was cleared. UNDISPATCHED because RxBus has no replay, so a scheduled collector could miss + // an exit sent before it starts. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } } override fun onDestroy() { - disposable.clear() + scope.cancel() super.onDestroy() } diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSPluginTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSPluginTest.kt index b8c4e980266a..4b50e0a5d0aa 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSPluginTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/DanaRSPluginTest.kt @@ -282,8 +282,8 @@ class DanaRSPluginTest : DanaRSTestBase() { danaRSPlugin = DanaRSPlugin( - aapsLogger, rh, preferences, commandQueue, aapsSchedulers, rxBus, context, danaPump, detailedBolusInfoStorage, temporaryBasalStorage, - fabricPrivacy, dateUtil, danaHistoryDatabase, decimalFormatter, pumpEnactResultProvider, blePreCheck, bolusProgressData + aapsLogger, rh, preferences, commandQueue, rxBus, context, danaPump, detailedBolusInfoStorage, temporaryBasalStorage, + dateUtil, danaHistoryDatabase, decimalFormatter, pumpEnactResultProvider, blePreCheck, bolusProgressData ) } diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/comm/DanaRsPacketNotifyDeliveryRateDisplayTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/comm/DanaRsPacketNotifyDeliveryRateDisplayTest.kt index d2c57a366155..3d63d23341e8 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/comm/DanaRsPacketNotifyDeliveryRateDisplayTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/comm/DanaRsPacketNotifyDeliveryRateDisplayTest.kt @@ -52,13 +52,11 @@ class DanaRsPacketNotifyDeliveryRateDisplayTest : DanaRSTestBase() { rh, preferences, commandQueue, - aapsSchedulers, rxBus, context, danaPump, detailedBolusInfoStorage, temporaryBasalStorage, - fabricPrivacy, dateUtil, danaHistoryDatabase, decimalFormatter, diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt index b4d09eaaf573..684e279f6ca2 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt @@ -94,8 +94,8 @@ internal class DanaRSOverviewViewModelTest { // rx / persistence wiring touched at construction whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toObservable(EventDanaRNewStatus::class.java)).thenReturn(Observable.empty()) - whenever(rxBus.toObservable(EventInitializationChanged::class.java)).thenReturn(Observable.empty()) + whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) @@ -127,7 +127,7 @@ internal class DanaRSOverviewViewModelTest { } private fun createViewModel() = DanaRSOverviewViewModel( - aapsLogger, rh, rxBus, aapsSchedulers, commandQueue, dateUtil, danaPump, + aapsLogger, rh, rxBus, commandQueue, dateUtil, danaPump, activePlugin, ch, persistenceLayer, danaRSPlugin, uel, preferences, bleTransport, context ) diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt index 23e6a30d8f12..d9bcfa2db10a 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/services/DanaRSServiceTest.kt @@ -52,7 +52,6 @@ class DanaRSServiceTest : TestBaseWithProfile() { fun setup() { danaRSService = DanaRSService() danaRSService.aapsLogger = aapsLogger - danaRSService.aapsSchedulers = aapsSchedulers danaRSService.rxBus = rxBus danaRSService.preferences = preferences danaRSService.rh = rh @@ -64,7 +63,6 @@ class DanaRSServiceTest : TestBaseWithProfile() { danaRSService.activePlugin = activePlugin danaRSService.uiInteraction = uiInteraction danaRSService.bleComm = bleComm - danaRSService.fabricPrivacy = fabricPrivacy danaRSService.pumpSync = pumpSync danaRSService.dateUtil = dateUtil danaRSService.bolusProgressData = BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)) From 717f918badf41f125f731863e035298749be425a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 17:21:18 +0200 Subject: [PATCH 092/146] DanaR pair wizard: listen with Flow instead of Rx Observable Converts the two remaining rxBus.toObservable subscriptions in DanaRPairWizardViewModel, which the Dana commit before this one missed. They collect on viewModelScope like the other Dana view models. The test drove the pairing state machine by pushing onto two PublishSubjects and reading the state right after, which worked because aapsSchedulers.main was the trampoline. The collectors now run on Main, so the subjects become MutableSharedFlows and a small emitAndRun helper sends the event and drains the test dispatcher. The dispatcher still stays idle when no event is sent, so pair()'s deferred readStatus launch keeps the assertions deterministic. --- .../danar/compose/DanaRPairWizardViewModel.kt | 27 ++++------- .../compose/DanaRPairWizardViewModelTest.kt | 47 ++++++++++--------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt index 9e38f18931f3..b3551657c3f7 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt @@ -9,8 +9,8 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.rfcomm.RfcommTransport import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.keys.interfaces.Preferences import app.aaps.pump.dana.DanaPump @@ -18,8 +18,7 @@ import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.dana.keys.DanaIntNonKey import app.aaps.pump.dana.keys.DanaStringNonKey import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -55,7 +54,6 @@ class DanaRPairWizardViewModel @Inject constructor( private val commandQueue: CommandQueue, private val pumpSync: PumpSync, private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, private val rfcommTransport: RfcommTransport ) : ViewModel() { @@ -65,7 +63,6 @@ class DanaRPairWizardViewModel @Inject constructor( private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events - private val disposable = CompositeDisposable() /** Dana device name pattern: 3 letters + 5 digits + 2 letters */ private val danaNamePattern = Regex("^[a-zA-Z]{3}[0-9]{5}[a-zA-Z]{2}(_[a-zA-Z])?$") @@ -85,14 +82,13 @@ class DanaRPairWizardViewModel @Inject constructor( } } } - disposable += rxBus - .toObservable(EventDanaRNewStatus::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ onPumpStatusUpdate() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) - disposable += rxBus - .toObservable(EventInitializationChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ onPumpStatusUpdate() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) + // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model + // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss a status sent before it starts. + rxBus.toFlow(EventDanaRNewStatus::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { onPumpStatusUpdate() } + rxBus.toFlow(EventInitializationChanged::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { onPumpStatusUpdate() } } fun reset() { @@ -164,9 +160,4 @@ class DanaRPairWizardViewModel @Inject constructor( fun finish() { _events.tryEmit(DanaRPairWizardEvent.Finish) } - - override fun onCleared() { - super.onCleared() - disposable.clear() - } } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt index 1a3db0bb92d6..79a5783d28b7 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt @@ -6,7 +6,6 @@ import app.aaps.core.interfaces.pump.rfcomm.RfcommDevice import app.aaps.core.interfaces.pump.rfcomm.RfcommTransport import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.keys.interfaces.Preferences @@ -15,10 +14,9 @@ import app.aaps.pump.dana.events.EventDanaRNewStatus import app.aaps.pump.dana.keys.DanaIntNonKey import app.aaps.pump.dana.keys.DanaStringNonKey import com.google.common.truth.Truth.assertThat -import io.reactivex.rxjava3.schedulers.Schedulers -import io.reactivex.rxjava3.subjects.PublishSubject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain @@ -34,10 +32,11 @@ import org.mockito.kotlin.whenever /** * Unit test for [DanaRPairWizardViewModel]. The pure state-mutating setters are asserted directly; the * pairing state machine (`pair()` → CONNECTING, then a pump-status update → COMPLETE / ERROR) is driven - * by emitting onto the `rxBus` streams the view-model subscribes to in `init`, exactly as the on-device - * handshake would. `aapsSchedulers.main` is the trampoline so those emissions deliver synchronously, and - * `pair()`'s `viewModelScope.launch { readStatus(...) }` stays deferred under a StandardTestDispatcher, so - * every assertion is deterministic and synchronous. + * by emitting onto the `rxBus` streams the view-model collects in `init`, exactly as the on-device + * handshake would. The collectors run on viewModelScope, which is Main, so `emitAndRun` hands the + * emission to the test dispatcher and then drains it. Without an emission the dispatcher stays idle, + * which keeps `pair()`'s `viewModelScope.launch { readStatus(...) }` deferred, so every assertion is + * deterministic. */ @OptIn(ExperimentalCoroutinesApi::class) internal class DanaRPairWizardViewModelTest { @@ -48,29 +47,35 @@ internal class DanaRPairWizardViewModelTest { private val commandQueue: CommandQueue = mock() private val pumpSync: PumpSync = mock() private val rxBus: RxBus = mock() - private val aapsSchedulers: AapsSchedulers = mock() private val rfcommTransport: RfcommTransport = mock() private val danaPump: DanaPump = mock() - // Real subjects so the two init collectors can be driven; the CONNECTING→COMPLETE/ERROR transition + // Real flows so the two init collectors can be driven; the CONNECTING→COMPLETE/ERROR transition // is only reachable by emitting a pump-status event (see onPumpStatusUpdate). - private val statusSubject: PublishSubject = PublishSubject.create() - private val initSubject: PublishSubject = PublishSubject.create() + private val statusFlow = MutableSharedFlow(extraBufferCapacity = 1) + private val initFlow = MutableSharedFlow(extraBufferCapacity = 1) private lateinit var sut: DanaRPairWizardViewModel + private val testDispatcher = StandardTestDispatcher() + + /** Sends one event and runs the collector it wakes up, so the state change is visible right away. */ + private fun emitAndRun(flow: MutableSharedFlow, event: T) { + assertThat(flow.tryEmit(event)).isTrue() + testDispatcher.scheduler.advanceUntilIdle() + } + @BeforeEach fun setUp() { - Dispatchers.setMain(StandardTestDispatcher()) + Dispatchers.setMain(testDispatcher) // init -> reset() reads these prefs synchronously (a mock String default is null -> NPE). whenever(preferences.get(DanaIntNonKey.Password)).thenReturn(0) whenever(preferences.get(DanaStringNonKey.RName)).thenReturn("") // init -> reset() -> refreshBondedDevices() reads the transport synchronously. whenever(rfcommTransport.getBondedDevices()).thenReturn(emptyList()) - // init subscribes to these rx streams via aapsSchedulers.main. - whenever(rxBus.toObservable(EventDanaRNewStatus::class.java)).thenReturn(statusSubject) - whenever(rxBus.toObservable(EventInitializationChanged::class.java)).thenReturn(initSubject) - whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) + // init collects these two streams on viewModelScope (Main = the test dispatcher below). + whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(statusFlow) + whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(initFlow) whenever(rh.gs(anyInt())).thenReturn("device changed") sut = buildViewModel() @@ -80,7 +85,7 @@ internal class DanaRPairWizardViewModelTest { fun tearDown() = Dispatchers.resetMain() private fun buildViewModel() = DanaRPairWizardViewModel( - aapsLogger, rh, preferences, danaPump, commandQueue, pumpSync, rxBus, aapsSchedulers, rfcommTransport + aapsLogger, rh, preferences, danaPump, commandQueue, pumpSync, rxBus, rfcommTransport ) /** Selects a device + password and calls `pair()`, leaving the wizard on the CONNECTING step. */ @@ -207,7 +212,7 @@ internal class DanaRPairWizardViewModelTest { moveToConnecting() whenever(danaPump.isPasswordOK).thenReturn(true) - statusSubject.onNext(EventDanaRNewStatus()) + emitAndRun(statusFlow, EventDanaRNewStatus()) val state = sut.uiState.value assertThat(state.step).isEqualTo(PairWizardStep.COMPLETE) @@ -220,7 +225,7 @@ internal class DanaRPairWizardViewModelTest { moveToConnecting() whenever(danaPump.isPasswordOK).thenReturn(false) - statusSubject.onNext(EventDanaRNewStatus()) + emitAndRun(statusFlow, EventDanaRNewStatus()) val state = sut.uiState.value assertThat(state.step).isEqualTo(PairWizardStep.ERROR) @@ -232,7 +237,7 @@ internal class DanaRPairWizardViewModelTest { moveToConnecting() whenever(danaPump.isPasswordOK).thenReturn(true) - initSubject.onNext(EventInitializationChanged()) + emitAndRun(initFlow, EventInitializationChanged()) assertThat(sut.uiState.value.step).isEqualTo(PairWizardStep.COMPLETE) } @@ -241,7 +246,7 @@ internal class DanaRPairWizardViewModelTest { fun `a status update while still on CONFIGURE is ignored`() { whenever(danaPump.isPasswordOK).thenReturn(true) - statusSubject.onNext(EventDanaRNewStatus()) + emitAndRun(statusFlow, EventDanaRNewStatus()) assertThat(sut.uiState.value.step).isEqualTo(PairWizardStep.CONFIGURE) } From 9c5d72103d6c0adc0043ceb6d5a170e21822a0c2 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 17:50:13 +0200 Subject: [PATCH 093/146] Diaconn: listen with Flow instead of Rx Observable Converts the 7 rxBus.toObservable subscriptions in :pump:diaconn, using the same shapes as the Dana drivers. DiaconnG8Plugin gets its own scope, created in onStart and cancelled in onStop where the CompositeDisposable was cleared. DiaconnG8Service already had such a scope for a preference observer and reuses it. The two view models collect on viewModelScope. All of them use CoroutineStart.UNDISPATCHED, because RxBus has no replay: a scheduled collector could miss an event sent before it starts. DiaconnHistoryViewModel keeps its CompositeDisposable - it also observes the Room history DAO, which is a separate RxJava source and not part of this change. Dead AapsSchedulers, FabricPrivacy and CompositeDisposable members and their imports are removed where nothing else used them, with the matching test constructor calls. The view model tests now stub toFlow instead of toObservable. --- .../app/aaps/pump/diaconn/DiaconnG8Plugin.kt | 47 +++++++++---------- .../compose/DiaconnHistoryViewModel.kt | 15 +++--- .../compose/DiaconnOverviewViewModel.kt | 27 ++++------- .../pump/diaconn/service/DiaconnG8Service.kt | 19 +++----- .../aaps/pump/diaconn/DiaconnG8PluginTest.kt | 2 +- .../compose/DiaconnHistoryViewModelTest.kt | 3 +- .../compose/DiaconnOverviewViewModelTest.kt | 6 +-- 7 files changed, 54 insertions(+), 65 deletions(-) diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt index d91ba581d12e..3d975edcc876 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt @@ -34,14 +34,13 @@ import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.pump.mapState import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventConfigBuilderChange import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.Round -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.icons.IcPluginDiaconn import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -54,8 +53,11 @@ import app.aaps.pump.diaconn.keys.DiaconnIntNonKey import app.aaps.pump.diaconn.keys.DiaconnIntentKey import app.aaps.pump.diaconn.keys.DiaconnStringNonKey import app.aaps.pump.diaconn.service.DiaconnG8Service -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -76,9 +78,7 @@ class DiaconnG8Plugin @Inject constructor( private val pumpSync: PumpSync, private val detailedBolusInfoStorage: DetailedBolusInfoStorage, private val temporaryBasalStorage: TemporaryBasalStorage, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, - private val aapsSchedulers: AapsSchedulers, private val diaconnHistoryDatabase: DiaconnHistoryDatabase, private val pumpEnactResultProvider: Provider, private val bolusProgressData: BolusProgressData, @@ -102,7 +102,7 @@ class DiaconnG8Plugin @Inject constructor( aapsLogger, rh, preferences, commandQueue ), Pump, Diaconn, PumpPluginConstraints, OwnDatabasePlugin { - private val disposable = CompositeDisposable() + private var scope: CoroutineScope? = null private var diaconnG8Service: DiaconnG8Service? = null private var mDeviceAddress = "" var mDeviceName = "" @@ -112,28 +112,27 @@ class DiaconnG8Plugin @Inject constructor( super.onStart() val intent = Intent(context, DiaconnG8Service::class.java) context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }) { fabricPrivacy.logException(it) } - - disposable += rxBus - .toObservable(EventConfigBuilderChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe { diaconnG8Pump.reset() } - disposable += rxBus - .toObservable(EventDiaconnG8DeviceChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - pumpSync.connectNewPump() - changePump() - }) { fabricPrivacy.logException(it) } + // Own scope on IO, like the io scheduler used before, cancelled in onStop like the + // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss an event sent before it starts. + val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } + rxBus.toFlow(EventConfigBuilderChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { diaconnG8Pump.reset() } + rxBus.toFlow(EventDiaconnG8DeviceChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { + pumpSync.connectNewPump() + changePump() + } changePump() // load device name on app start } override suspend fun onStop() { + scope?.cancel() + scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt index 625e00d4f721..da66e8d299d3 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter @@ -25,6 +26,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -63,12 +65,13 @@ class DiaconnHistoryViewModel @Inject constructor( _uiState.value = PumpHistoryUiState(availableTypes = types, selectedType = types.firstOrNull()) - disposable += rxBus - .toObservable(EventPumpStatusChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ event -> - _uiState.update { it.copy(statusMessage = rh.gs(event.getStatus())) } - }, { aapsLogger.error(LTag.PUMP, "Error", it) }) + // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model + // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss a status sent before it starts. + rxBus.toFlow(EventPumpStatusChanged::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> + _uiState.update { it.copy(statusMessage = rh.gs(event.getStatus())) } + } types.firstOrNull()?.let { loadRecords(it.type) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt index c4d03aa9cde4..18276cece2bc 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt @@ -24,8 +24,8 @@ import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.pump.PumpRate import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.interfaces.Preferences @@ -43,8 +43,7 @@ import app.aaps.pump.diaconn.events.EventDiaconnG8NewStatus import app.aaps.pump.diaconn.keys.DiaconnStringNonKey import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -73,7 +72,6 @@ class DiaconnOverviewViewModel @Inject constructor( private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val rxBus: RxBus, - aapsSchedulers: AapsSchedulers, private val commandQueue: CommandQueue, private val dateUtil: DateUtil, private val diaconnG8Pump: DiaconnG8Pump, @@ -85,7 +83,6 @@ class DiaconnOverviewViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { - private val disposable = CompositeDisposable() private val _events = MutableSharedFlow(extraBufferCapacity = 5) val events: SharedFlow = _events @@ -95,14 +92,13 @@ class DiaconnOverviewViewModel @Inject constructor( private val rxTrigger = MutableStateFlow(0L) init { - disposable += rxBus - .toObservable(EventDiaconnG8NewStatus::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ rxTrigger.value = System.currentTimeMillis() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) - disposable += rxBus - .toObservable(EventInitializationChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ rxTrigger.value = System.currentTimeMillis() }, { aapsLogger.error(LTag.PUMP, "Error", it) }) + // viewModelScope dies with the view model like the CompositeDisposable did. The bodies only + // write a timestamp, so the dispatcher does not matter. UNDISPATCHED because RxBus has no + // replay, so a scheduled collector could miss an event sent before it starts. + rxBus.toFlow(EventDiaconnG8NewStatus::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } + rxBus.toFlow(EventInitializationChanged::class.java) + .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } persistenceLayer.observeChanges(EB::class.java) .onEach { rxTrigger.value = System.currentTimeMillis() } @@ -144,11 +140,6 @@ class DiaconnOverviewViewModel @Inject constructor( }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), buildInitialState()) - override fun onCleared() { - super.onCleared() - disposable.clear() - } - fun onRefreshClick() { aapsLogger.debug(LTag.PUMP, "Clicked connect to pump") diaconnG8Pump.lastConnection = 0 diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt index a78c13fe7fe9..0f6b24aea11d 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt @@ -24,15 +24,14 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.Command import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.diaconn.DiaconnG8Plugin @@ -77,8 +76,7 @@ import app.aaps.pump.diaconn.packet.TimeSettingPacket import app.aaps.pump.diaconn.pumplog.PumpLogUtil import dagger.android.DaggerService import dagger.android.HasAndroidInjector -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -110,10 +108,8 @@ class DiaconnG8Service : DaggerService() { @Inject lateinit var diaconnG8Pump: DiaconnG8Pump @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var bleCommonService: BLECommonService - @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var pumpSync: PumpSync @Inject lateinit var dateUtil: DateUtil - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var diaconnLogUploader: DiaconnLogUploader @Inject lateinit var diaconnHistoryRecordDao: DiaconnHistoryRecordDao @Inject lateinit var uiInteraction: UiInteraction @@ -122,7 +118,6 @@ class DiaconnG8Service : DaggerService() { @Inject lateinit var ch: ConcentrationHelper @Inject lateinit var bolusProgressData: BolusProgressData - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null private val mBinder: IBinder = LocalBinder() private var lastApproachingDailyLimit: Long = 0 @@ -133,10 +128,11 @@ class DiaconnG8Service : DaggerService() { super.onCreate() val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ stopSelf() }, fabricPrivacy::logException) + // Same scope as the preference observer below: IO, like the io scheduler used before, and + // cancelled in onDestroy like the CompositeDisposable was cleared. UNDISPATCHED because + // RxBus has no replay, so a scheduled collector could miss an exit sent before it starts. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } preferences.observe(DiaconnIntKey.BolusSpeed).drop(1).onEach { diaconnG8Pump.bolusSpeed = preferences.get(DiaconnIntKey.BolusSpeed) diaconnG8Pump.speed = preferences.get(DiaconnIntKey.BolusSpeed) @@ -161,7 +157,6 @@ class DiaconnG8Service : DaggerService() { override fun onDestroy() { scope?.cancel() scope = null - disposable.clear() super.onDestroy() } diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt index 6235c0432b32..35ed333917e4 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/DiaconnG8PluginTest.kt @@ -45,7 +45,7 @@ class DiaconnG8PluginTest : TestBaseWithProfile() { diaconnG8Pump = DiaconnG8Pump(aapsLogger, dateUtil, decimalFormatter) diaconnG8Plugin = DiaconnG8Plugin( aapsLogger, rh, preferences, commandQueue, rxBus, context, diaconnG8Pump, - pumpSync, detailedBolusInfoStorage, temporaryBasalStorage, fabricPrivacy, dateUtil, aapsSchedulers, + pumpSync, detailedBolusInfoStorage, temporaryBasalStorage, dateUtil, diaconnHistoryDatabase, pumpEnactResultProvider, BolusProgressData(ch, CoroutineScope(Dispatchers.Unconfined)), blePreCheck ) } diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt index c63a4136c92c..335bae1a9018 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt @@ -16,6 +16,7 @@ import com.google.common.truth.Truth.assertThat import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -56,7 +57,7 @@ internal class DiaconnHistoryViewModelTest { // init runs synchronously (not in a launch): builds the type list, subscribes to status events, // and loads records for the first type — every collaborator it touches must be stubbed. whenever(rh.gs(anyInt())).thenReturn("") - whenever(rxBus.toObservable(EventPumpStatusChanged::class.java)).thenReturn(Observable.empty()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(dateUtil.now()).thenReturn(0L) diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt index aca27aba4ceb..009b15ad33e9 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt @@ -86,8 +86,8 @@ internal class DiaconnOverviewViewModelTest { // rx wiring touched at construction (PumpCommunicationStatus + init subscriptions) whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toObservable(EventDiaconnG8NewStatus::class.java)).thenReturn(Observable.empty()) - whenever(rxBus.toObservable(EventInitializationChanged::class.java)).thenReturn(Observable.empty()) + whenever(rxBus.toFlow(EventDiaconnG8NewStatus::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) @@ -128,7 +128,7 @@ internal class DiaconnOverviewViewModelTest { } private fun createViewModel() = DiaconnOverviewViewModel( - aapsLogger, rh, rxBus, aapsSchedulers, commandQueue, dateUtil, diaconnG8Pump, + aapsLogger, rh, rxBus, commandQueue, dateUtil, diaconnG8Pump, activePlugin, persistenceLayer, uel, preferences, ch, context ) From f1ac697e12d81d7231f4a699d29d2dc998eca5e3 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 17:54:00 +0200 Subject: [PATCH 094/146] Omnipod Eros: listen with Flow instead of Rx Observable Converts the 7 rxBus.toObservable subscriptions in OmnipodErosPumpPlugin. The plugin already created a scope in onStart for its preference observers, which were on collectResilient before this change, so the bus collectors reuse it. The scope now starts before them instead of after, and onStop cancels it where the CompositeDisposable was cleared. All of them use CoroutineStart.UNDISPATCHED, because RxBus has no replay: a scheduled collector could miss an event sent before it starts. EventAppInitialized in particular is sent once at startup, so it must not be missed. Dead AapsSchedulers and CompositeDisposable members and their imports are removed; FabricPrivacy stays, it still reports the init event. --- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 86 ++++++++----------- .../omnipod/eros/OmnipodErosPumpPluginTest.kt | 3 +- 2 files changed, 37 insertions(+), 52 deletions(-) diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index 4d6ff53e86f0..82413ea2a4dc 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -42,7 +42,6 @@ import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.queue.CustomCommand import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit @@ -99,9 +98,8 @@ import app.aaps.pump.omnipod.eros.rileylink.service.RileyLinkOmnipodService import app.aaps.pump.omnipod.eros.ui.compose.OmnipodErosComposeContent import app.aaps.pump.omnipod.eros.util.AapsOmnipodUtil import app.aaps.pump.omnipod.eros.util.OmnipodAlertUtil -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -132,7 +130,6 @@ class OmnipodErosPumpPlugin @Inject constructor( rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, - private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, private val context: Context, private val podStateManager: ErosPodStateManager, @@ -167,7 +164,6 @@ class OmnipodErosPumpPlugin @Inject constructor( aapsLogger, rh, preferences, commandQueue ), Pump, RileyLinkPumpDevice, OmnipodEros, OwnDatabasePlugin { - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null private val displayConnectionMessages = false private val statusChecker: Runnable @@ -257,33 +253,26 @@ class OmnipodErosPumpPlugin @Inject constructor( val intent = Intent(context, RileyLinkOmnipodService::class.java) serviceConnection?.let { context.bindService(intent, it, Context.BIND_AUTO_CREATE) } - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ serviceConnection?.let { context.unbindService(it) } }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventOmnipodErosTbrChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ handleCancelledTbr() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventOmnipodErosUncertainTbrRecovered::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ handleUncertainTbrRecovery() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventOmnipodErosActiveAlertsChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ handleActivePodAlerts() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventOmnipodErosFaultEventChanged::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ handlePodFaultEvent() }, fabricPrivacy::logException) - // Pass only to setup wizard - disposable += rxBus - .toObservable(EventRileyLinkDeviceStatusChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ event -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) }, fabricPrivacy::logException) + // Same scope as the preference observers below: IO, like the io scheduler used before, and + // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus + // has no replay, so a scheduled collector could miss an event sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { serviceConnection?.let { context.unbindService(it) } } + rxBus.toFlow(EventOmnipodErosTbrChanged::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleCancelledTbr() } + rxBus.toFlow(EventOmnipodErosUncertainTbrRecovered::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleUncertainTbrRecovery() } + rxBus.toFlow(EventOmnipodErosActiveAlertsChanged::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleActivePodAlerts() } + rxBus.toFlow(EventOmnipodErosFaultEventChanged::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handlePodFaultEvent() } + // Pass only to setup wizard + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> + rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) + } merge( preferences.observe(OmnipodBooleanPreferenceKey.BasalBeepsEnabled).drop(1).map {}, preferences.observe(OmnipodBooleanPreferenceKey.BolusBeepsEnabled).drop(1).map {}, @@ -310,25 +299,23 @@ class OmnipodErosPumpPlugin @Inject constructor( commandQueue.customCommand(CommandUpdateAlertConfiguration()) } } - disposable += rxBus - .toObservable(EventAppInitialized::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ - // See if a bolus was active before the app previously exited - // If so, add it to history - // Needs to be done after EventAppInitialized because otherwise, TreatmentsPlugin.onStart() hasn't been called yet - // so it didn't initialize a TreatmentService yet, resulting in a NullPointerException - if (preferences.getIfExists(ErosStringNonPreferenceKey.ActiveBolus) != null) { - val activeBolusString = preferences.get(ErosStringNonPreferenceKey.ActiveBolus) - aapsLogger.warn(LTag.PUMP, "Found active bolus in preferences: {}. Adding Treatment.", activeBolusString) - try { - aapsOmnipodErosManager.addBolusToHistory(DetailedBolusInfo().fromJsonString(activeBolusString)) - } catch (ex: Exception) { - aapsLogger.error(LTag.PUMP, "Failed to add active bolus to history", ex) - } - preferences.remove(ErosStringNonPreferenceKey.ActiveBolus) - } - }, fabricPrivacy::logException) + rxBus.toFlow(EventAppInitialized::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { + // See if a bolus was active before the app previously exited + // If so, add it to history + // Needs to be done after EventAppInitialized because otherwise, TreatmentsPlugin.onStart() hasn't been called yet + // so it didn't initialize a TreatmentService yet, resulting in a NullPointerException + if (preferences.getIfExists(ErosStringNonPreferenceKey.ActiveBolus) != null) { + val activeBolusString = preferences.get(ErosStringNonPreferenceKey.ActiveBolus) + aapsLogger.warn(LTag.PUMP, "Found active bolus in preferences: {}. Adding Treatment.", activeBolusString) + try { + aapsOmnipodErosManager.addBolusToHistory(DetailedBolusInfo().fromJsonString(activeBolusString)) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, "Failed to add active bolus to history", ex) + } + preferences.remove(ErosStringNonPreferenceKey.ActiveBolus) + } + } } override fun isRileyLinkReady(): Boolean = rileyLinkServiceData.rileyLinkServiceState.isReady() @@ -409,7 +396,6 @@ class OmnipodErosPumpPlugin @Inject constructor( handler = null serviceConnection?.let { context.unbindService(it) } serviceConnection = null - disposable.clear() } private fun queueAcknowledgeAlertsCommand() { diff --git a/pump/omnipod/eros/src/test/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPluginTest.kt b/pump/omnipod/eros/src/test/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPluginTest.kt index 4f06a29ca7d3..154ae228f7ec 100644 --- a/pump/omnipod/eros/src/test/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPluginTest.kt +++ b/pump/omnipod/eros/src/test/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPluginTest.kt @@ -13,7 +13,6 @@ import app.aaps.pump.omnipod.eros.manager.AapsOmnipodErosManager import app.aaps.pump.omnipod.eros.util.AapsOmnipodUtil import app.aaps.pump.omnipod.eros.util.OmnipodAlertUtil import app.aaps.shared.tests.TestBaseWithProfile -import app.aaps.shared.tests.rx.TestAapsSchedulers import kotlinx.coroutines.runBlocking import org.joda.time.DateTimeZone import org.joda.time.tz.UTCProvider @@ -48,7 +47,7 @@ class OmnipodErosPumpPluginTest : TestBaseWithProfile() { .thenReturn("") plugin = OmnipodErosPumpPlugin( - aapsLogger, rh, preferences, commandQueue, TestAapsSchedulers(), rxBus, context, + aapsLogger, rh, preferences, commandQueue, rxBus, context, erosPodStateManager, aapsOmnipodErosManager, fabricPrivacy, rileyLinkServiceData, aapsOmnipodUtil, rileyLinkUtil, omnipodAlertUtil, pumpSync, uiInteraction, notificationManager, erosHistoryDatabase, pumpEnactResultProvider, protectionCheck, blePreCheck From dd79366495ec8903697e9a79c62e57382966ea88 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 18:04:54 +0200 Subject: [PATCH 095/146] Remaining pumps: listen with Flow instead of Rx Observable Converts the last 6 rxBus.toObservable call sites, in :pump:common, :pump:medtronic, :pump:medtrum, :pump:eopatch and :pump:insight. No call site is left in the whole project now, only the RxBus declaration and its implementation. PumpPluginAbstract and MedtronicPumpPlugin get a scope created in onStart and cancelled in onStop. MedtrumPlugin and MedtrumService reuse the scope they already had for their preference observers. PatchManager collects on an app lifetime scope, because its CompositeDisposable is never cleared - it is a singleton that never tears down. InsightOverviewState creates its scope in start() and cancels it in stop(), the same pair the CompositeDisposable used. It must not use the injected appScope, which is app lifetime. It had no logger, so InsightComposeContent now passes one down from the plugin. All of them use CoroutineStart.UNDISPATCHED, because RxBus has no replay: a scheduled collector could miss an event sent before it starts. Dead AapsSchedulers, FabricPrivacy and CompositeDisposable members and their imports are removed where nothing else used them, with the matching test constructor calls. PatchManager keeps both - it still has other RxJava sources. --- .../aaps/pump/common/PumpPluginAbstract.kt | 31 +++++++++------- .../app/aaps/pump/eopatch/ble/PatchManager.kt | 35 +++++++++++-------- .../app/aaps/pump/insight/InsightPlugin.kt | 2 +- .../insight/compose/InsightComposeContent.kt | 33 ++++++++++------- .../compose/InsightOverviewStateTest.kt | 6 ++-- .../pump/medtronic/MedtronicPumpPlugin.kt | 23 ++++++------ .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 18 ++++------ .../pump/medtrum/services/MedtrumService.kt | 18 ++++------ 8 files changed, 86 insertions(+), 80 deletions(-) diff --git a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt index ac0b09081465..a0ad92290258 100644 --- a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt +++ b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt @@ -26,13 +26,12 @@ import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.pump.mapState import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventCustomActionsChanged import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.LongNonPreferenceKey import app.aaps.core.keys.interfaces.NonPreferenceKey import app.aaps.core.keys.interfaces.Preferences @@ -45,7 +44,11 @@ import app.aaps.pump.common.driver.refresh.PumpDataRefreshType import app.aaps.pump.common.sync.PumpDbEntryCarbs import app.aaps.pump.common.sync.PumpSyncEntriesCreator import app.aaps.pump.common.sync.PumpSyncStorage -import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.runBlocking import javax.inject.Provider @@ -64,8 +67,6 @@ abstract class PumpPluginAbstract protected constructor( commandQueue: CommandQueue, var rxBus: RxBus, var context: Context, - var fabricPrivacy: FabricPrivacy, - var aapsSchedulers: AapsSchedulers, var pumpSync: PumpSync, var pumpSyncStorage: PumpSyncStorage, val pumpDriverConfigurationInternal: PumpDriverConfiguration, @@ -84,7 +85,7 @@ abstract class PumpPluginAbstract protected constructor( Pump, PluginConstraints, /*Constraints,*/ PumpSyncEntriesCreator { - protected val disposable = CompositeDisposable() + private var scope: CoroutineScope? = null // Pump capabilities final override var pumpDescription = PumpDescription() @@ -118,12 +119,15 @@ abstract class PumpPluginAbstract protected constructor( if (hasService()) { val intent = Intent(context, serviceClass) context.bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE) - disposable.add( - rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(serviceConnection!!) }, fabricPrivacy::logException) - ) + // Own scope on IO, like the io scheduler used before, cancelled in onStop like the + // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a + // scheduled collector could miss an exit sent before it starts. + val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { + context.unbindService(serviceConnection!!) + } } serviceRunning = true onStartScheduledPumpActions() @@ -137,7 +141,8 @@ abstract class PumpPluginAbstract protected constructor( } } serviceRunning = false - disposable.clear() + scope?.cancel() + scope = null super.onStop() } diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt index fda6d8091426..096c7237e784 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt @@ -4,6 +4,7 @@ import android.content.Context import app.aaps.core.data.model.TE import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.AlarmSound import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel @@ -14,6 +15,7 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventCustomActionsChanged import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.rx.events.EventRefreshOverview @@ -41,6 +43,10 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.functions.Function import io.reactivex.rxjava3.functions.Predicate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -68,6 +74,9 @@ class PatchManager @Inject constructor( private val compositeDisposable = CompositeDisposable() + // App lifetime, like the CompositeDisposable above: this is a singleton that never tears down. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var patchScanner: IPatchScanner = PatchScanner(context, aapsLogger) private var mConnectingDisposable: Disposable? = null @@ -100,20 +109,18 @@ class PatchManager @Inject constructor( } }) ) - compositeDisposable.add( - rxBus - .toObservable(EventPatchActivationNotComplete::class.java) - .observeOn(aapsSchedulers.io) - .subscribeOn(aapsSchedulers.main) - .subscribe(Consumer { - notificationManager.post( - id = NotificationId.EOFLOW_PATCH_ALERT, - text = rh.gs(R.string.patch_activate_reminder_desc), - level = NotificationLevel.URGENT, - sound = AlarmSound.ALARM - ) - }) - ) + // App lifetime scope, matching the CompositeDisposable here which is never cleared. IO, like + // observeOn(aapsSchedulers.io). UNDISPATCHED because RxBus has no replay, so a scheduled + // collector could miss an alert sent before it starts. + rxBus.toFlow(EventPatchActivationNotComplete::class.java) + .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { + notificationManager.post( + id = NotificationId.EOFLOW_PATCH_ALERT, + text = rh.gs(R.string.patch_activate_reminder_desc), + level = NotificationLevel.URGENT, + sound = AlarmSound.ALARM + ) + } } override fun init() { diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt index 0bdd3484e1f1..2039ba05a9cd 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt @@ -165,12 +165,12 @@ class InsightPlugin @Inject constructor( .composeContent { plugin -> InsightComposeContent( insightPlugin = plugin as InsightPlugin, + aapsLogger = aapsLogger, rh = rh, rxBus = rxBus, dateUtil = dateUtil, commandQueue = commandQueue, context = context, - aapsSchedulers = aapsSchedulers, pumpSync = pumpSync, blePreCheck = blePreCheck, ch = ch, diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt index 3b169ceafd2a..a4195c946d32 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.interfaces.insulin.ConcentrationHelper +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.pump.BlePreCheck import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.pump.PumpRate @@ -29,8 +31,8 @@ import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.Callback import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.ui.compose.ComposablePluginContent import app.aaps.core.ui.compose.ToolbarConfig @@ -48,8 +50,11 @@ import app.aaps.pump.insight.descriptors.BolusType import app.aaps.pump.insight.descriptors.InsightState import app.aaps.pump.insight.descriptors.OperatingMode import app.aaps.pump.insight.events.EventLocalInsightUpdateGUI -import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -61,12 +66,12 @@ private enum class InsightScreen { OVERVIEW, PAIR_WIZARD } class InsightComposeContent( private val insightPlugin: InsightPlugin, + private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val rxBus: RxBus, private val dateUtil: DateUtil, private val commandQueue: CommandQueue, private val context: Context, - private val aapsSchedulers: AapsSchedulers, private val pumpSync: PumpSync, private val blePreCheck: BlePreCheck, private val ch: ConcentrationHelper, @@ -82,12 +87,12 @@ class InsightComposeContent( val overviewState = remember { InsightOverviewState( insightPlugin = insightPlugin, + aapsLogger = aapsLogger, rh = rh, rxBus = rxBus, dateUtil = dateUtil, commandQueue = commandQueue, context = context, - aapsSchedulers = aapsSchedulers, ch = ch, appScope = appScope ) @@ -246,17 +251,17 @@ internal sealed class InsightOverviewEvent { internal class InsightOverviewState( private val insightPlugin: InsightPlugin, + private val aapsLogger: AAPSLogger, private val rh: ResourceHelper, private val rxBus: RxBus, private val dateUtil: DateUtil, private val commandQueue: CommandQueue, @Suppress("unused") private val context: Context, - private val aapsSchedulers: AapsSchedulers, private val ch: ConcentrationHelper, private val appScope: CoroutineScope ) { - private val disposable = CompositeDisposable() + private var scope: CoroutineScope? = null private var refreshPending = false private var tbrOverNotificationPending = false @@ -267,16 +272,20 @@ internal class InsightOverviewState( val uiState: StateFlow = _uiState fun start() { - disposable.add( - rxBus.toObservable(EventLocalInsightUpdateGUI::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ refresh() }, {}) - ) + // Own scope on Main, like the main scheduler used before, cancelled in stop() like the + // CompositeDisposable was cleared. appScope must not be used here, it is app lifetime. + // UNDISPATCHED because RxBus has no replay, so a scheduled collector could miss an update + // sent before it starts. + val newScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + scope = newScope + rxBus.toFlow(EventLocalInsightUpdateGUI::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { refresh() } refresh() } fun stop() { - disposable.clear() + scope?.cancel() + scope = null } fun performUnpair() { diff --git a/pump/insight/src/test/kotlin/app/aaps/pump/insight/compose/InsightOverviewStateTest.kt b/pump/insight/src/test/kotlin/app/aaps/pump/insight/compose/InsightOverviewStateTest.kt index deb8ef9e5398..a929c655cab6 100644 --- a/pump/insight/src/test/kotlin/app/aaps/pump/insight/compose/InsightOverviewStateTest.kt +++ b/pump/insight/src/test/kotlin/app/aaps/pump/insight/compose/InsightOverviewStateTest.kt @@ -1,10 +1,10 @@ package app.aaps.pump.insight.compose import android.content.Context +import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.ui.compose.pump.PumpInfoRow @@ -35,11 +35,11 @@ import app.aaps.core.ui.R as CoreUiR internal class InsightOverviewStateTest { @Mock private lateinit var rh: ResourceHelper + @Mock private lateinit var aapsLogger: AAPSLogger @Mock private lateinit var rxBus: RxBus @Mock private lateinit var dateUtil: DateUtil @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var context: Context - @Mock private lateinit var aapsSchedulers: AapsSchedulers @Mock private lateinit var ch: ConcentrationHelper private val insightPlugin: InsightPlugin = mock() @@ -55,12 +55,12 @@ internal class InsightOverviewStateTest { private fun createState() = InsightOverviewState( insightPlugin = insightPlugin, + aapsLogger = aapsLogger, rh = rh, rxBus = rxBus, dateUtil = dateUtil, commandQueue = commandQueue, context = context, - aapsSchedulers = aapsSchedulers, ch = ch, appScope = appScope ) diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index 204d903af888..e017f2859ab0 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -29,15 +29,14 @@ import app.aaps.core.interfaces.pump.PumpSync.TemporaryBasalType import app.aaps.core.interfaces.pump.defs.determineCorrectBasalSize import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventRefreshButtonState import app.aaps.core.interfaces.rx.events.EventRefreshOverview import app.aaps.core.interfaces.rx.events.EventSWRLStatus import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.icons.IcPluginMedtronic import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef @@ -93,6 +92,7 @@ import app.aaps.pump.medtronic.service.RileyLinkMedtronicService import app.aaps.pump.medtronic.util.MedtronicUtil import app.aaps.pump.medtronic.util.MedtronicUtil.Companion.isSame import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -123,7 +123,6 @@ class MedtronicPumpPlugin @Inject constructor( commandQueue: CommandQueue, rxBus: RxBus, context: Context, - fabricPrivacy: FabricPrivacy, private val medtronicUtil: MedtronicUtil, private val medtronicPumpStatus: MedtronicPumpStatus, private val medtronicHistoryData: MedtronicHistoryData, @@ -132,7 +131,6 @@ class MedtronicPumpPlugin @Inject constructor( private val uiInteraction: UiInteraction, private val notificationManager: NotificationManager, dateUtil: DateUtil, - aapsSchedulers: AapsSchedulers, pumpSync: PumpSync, pumpSyncStorage: PumpSyncStorage, decimalFormatter: DecimalFormatter, @@ -165,9 +163,7 @@ class MedtronicPumpPlugin @Inject constructor( rxBus = rxBus, //activePlugin = activePlugin, context = context, - fabricPrivacy = fabricPrivacy, dateUtil = dateUtil, - aapsSchedulers = aapsSchedulers, pumpSync = pumpSync, pumpSyncStorage = pumpSyncStorage, decimalFormatter = decimalFormatter, @@ -210,15 +206,16 @@ class MedtronicPumpPlugin @Inject constructor( }.start() } } - // Pass only to setup wizard - disposable.add( - rxBus - .toObservable(EventRileyLinkDeviceStatusChange::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ event: EventRileyLinkDeviceStatusChange -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) }, fabricPrivacy::logException) - ) + // Same scope as the preference observer below: IO, like the io scheduler used before, and + // cancelled in onStop. UNDISPATCHED because RxBus has no replay, so a scheduled collector + // could miss a status sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope + // Pass only to setup wizard + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> + rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) + } preferences.observe(MedtronicStringPreferenceKey.Serial).drop(1).onEach { aapsLogger.debug(LTag.PUMP, "Medtronic serial number changed, reporting new pump") medtronicPumpStatus.serialNumber = preferences.getIfExists(MedtronicStringPreferenceKey.Serial) ?: "" diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index 0e482c3838ea..8650ef8e6d4f 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -30,13 +30,11 @@ import app.aaps.core.interfaces.pump.defs.fillFor import app.aaps.core.interfaces.pump.mapState import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef @@ -54,14 +52,13 @@ import app.aaps.pump.medtrum.keys.MedtrumLongNonKey import app.aaps.pump.medtrum.keys.MedtrumStringKey import app.aaps.pump.medtrum.keys.MedtrumStringNonKey import app.aaps.pump.medtrum.services.MedtrumService -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton import kotlin.math.abs import kotlin.math.min import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -74,10 +71,8 @@ class MedtrumPlugin @Inject constructor( rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, - private val aapsSchedulers: AapsSchedulers, private val rxBus: RxBus, private val context: Context, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, private val medtrumPump: MedtrumPump, private val temporaryBasalStorage: TemporaryBasalStorage, @@ -103,7 +98,6 @@ class MedtrumPlugin @Inject constructor( aapsLogger, rh, preferences, commandQueue ), Pump, Medtrum { - private val disposable = CompositeDisposable() private var scope: CoroutineScope? = null private var medtrumService: MedtrumService? = null @@ -113,12 +107,13 @@ class MedtrumPlugin @Inject constructor( medtrumPump.loadVarsFromSP() val intent = Intent(context, MedtrumService::class.java) context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ context.unbindService(mConnection) }, fabricPrivacy::logException) + // Same scope as the preference observer below: IO, like the io scheduler used before, and + // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus + // has no replay, so a scheduled collector could miss an exit sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } preferences.observe(MedtrumStringNonKey.SnInput).drop(1).collectResilient(newScope, aapsLogger, LTag.PUMP) { updateMaxInsulinLimitsForPumpType() } @@ -132,7 +127,6 @@ class MedtrumPlugin @Inject constructor( scope?.cancel() scope = null context.unbindService(mConnection) - disposable.clear() super.onStop() } diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt index 7b16660ea795..2cc7a67ca4ca 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt @@ -23,14 +23,12 @@ import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.pump.medtrum.MedtrumPlugin @@ -69,10 +67,9 @@ import app.aaps.pump.medtrum.keys.MedtrumStringNonKey import app.aaps.pump.medtrum.util.MedtrumSnUtil import dagger.android.DaggerService import dagger.android.HasAndroidInjector -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -86,7 +83,6 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { @Inject lateinit var injector: HasAndroidInjector @Inject lateinit var aapsLogger: AAPSLogger - @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var rxBus: RxBus @Inject lateinit var preferences: Preferences @Inject lateinit var rh: ResourceHelper @@ -98,7 +94,6 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { @Inject lateinit var uiInteraction: UiInteraction @Inject lateinit var notificationManager: NotificationManager @Inject lateinit var bleTransport: MedtrumBleTransport - @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var pumpSync: PumpSync @Inject lateinit var detailedBolusInfoStorage: DetailedBolusInfoStorage @Inject lateinit var dateUtil: DateUtil @@ -116,7 +111,6 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { private const val CHECK_EXPIRY_WARNING_TIME_MS = 5 * 60 * 1000L } - private val disposable = CompositeDisposable() private val mBinder: IBinder = LocalBinder() private var currentState: State = IdleState() @@ -132,10 +126,11 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { override fun onCreate() { super.onCreate() bleTransport.setMedtrumCallback(this) - disposable += rxBus - .toObservable(EventAppExit::class.java) - .observeOn(aapsSchedulers.io) - .subscribe({ stopSelf() }, fabricPrivacy::logException) + // Same service scope as the preference observers below, which is IO like the io scheduler + // used before. UNDISPATCHED because RxBus has no replay, so a scheduled collector could miss + // an exit sent before it starts. + rxBus.toFlow(EventAppExit::class.java) + .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } preferences.observe(MedtrumStringNonKey.SnInput).drop(1).collectResilient(scope, aapsLogger, LTag.PUMP) { aapsLogger.debug(LTag.PUMPCOMM, "Serial number changed, reporting new pump!") medtrumPump.loadUserSettingsFromSP() @@ -217,7 +212,6 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { override fun onDestroy() { super.onDestroy() - disposable.clear() scope.cancel() } From d2a92027995a8d17f5e13cc6b8c8a2136313c8e8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 18:21:32 +0200 Subject: [PATCH 096/146] RxBus: drop toObservable, the bus is Flow only now No caller was left after the pump drivers were converted, so the RxJava side of the bus goes away: the toObservable declaration, its implementation, and the PublishSubject behind it. RxBusImpl no longer needs AapsSchedulers either, since the Flow has no scheduler to subscribe on. The interface doc now says what the migration taught us: the bus has no replay, so a collector only sees what is sent after it starts, and subscriptions made from a constructor or onStart need CoroutineStart.UNDISPATCHED. Three test helpers still used the Rx side and move to collectors on an Unconfined scope, all UNDISPATCHED so they are subscribed before the code under test sends anything: - DataHandlerMobileWearBolusTest: the three capture helpers become one collectMobileToWear. - ProfileSwitchExpirySchedulerTest: the event counter. - DataHandlerWearTest: the pong capture. - RxHelper (androidTest): clear() uses cancelChildren, not cancel, because CompositeDisposable.clear() left its container usable and a cancelled scope would make every later listen() silently do nothing. The two remaining toObservable calls in eopatch are Single.toObservable(), plain RxJava conversions with nothing to do with the bus. --- .../kotlin/app/aaps/helpers/RxHelper.kt | 37 +++--- .../app/aaps/core/interfaces/rx/bus/RxBus.kt | 16 +-- .../compose/dialogs/GlobalDialogHostTest.kt | 3 - .../compose/dialogs/GlobalSnackbarHostTest.kt | 3 - .../ProfileSwitchExpirySchedulerTest.kt | 15 ++- .../DataHandlerMobileWearBolusTest.kt | 121 +++--------------- .../aaps/shared/impl/di/SharedImplModule.kt | 2 +- .../app/aaps/shared/impl/rx/bus/RxBusImpl.kt | 15 +-- .../kotlin/app/aaps/shared/tests/TestBase.kt | 2 +- .../app/aaps/wear/comm/DataHandlerWearTest.kt | 14 +- 10 files changed, 70 insertions(+), 158 deletions(-) diff --git a/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt b/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt index 349c1585a4ef..2a919d7dbc7a 100644 --- a/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt +++ b/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt @@ -3,13 +3,15 @@ package app.aaps.helpers import app.aaps.core.data.time.T import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.collectResilient import app.aaps.core.interfaces.rx.events.Event import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.interfaces.utils.fabric.FabricPrivacy -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -21,15 +23,15 @@ import javax.inject.Inject */ class RxHelper @Inject constructor( private val rxBus: RxBus, - private val aapsSchedulers: AapsSchedulers, - private val fabricPrivacy: FabricPrivacy, private val dateUtil: DateUtil, private val aapsLogger: AAPSLogger ) { private val hashMap = HashMap, AtomicBoolean>() private val eventHashMap = HashMap, Event>() - private val disposable = CompositeDisposable() + + // Lives as long as the helper; clear() cancels its collectors, like clearing the CompositeDisposable. + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) /** * Register class for listening @@ -40,15 +42,14 @@ class RxHelper @Inject constructor( fun listen(clazz: Class): AtomicBoolean = hashMap[clazz] ?: AtomicBoolean(false).also { ab -> hashMap[clazz] = ab - // Setup RxBus tracking - disposable += rxBus - .toObservable(clazz) - .observeOn(aapsSchedulers.io) - .subscribe({ - aapsLogger.info(LTag.EVENTS, "==>> ${clazz.simpleName} registered") - ab.set(true) - eventHashMap[clazz] = it - }, fabricPrivacy::logException) + // Setup RxBus tracking. UNDISPATCHED because RxBus has no replay: a test that sends an + // event right after listen() returns must not race the collector starting. + rxBus.toFlow(clazz) + .collectResilient(scope, aapsLogger, LTag.EVENTS, start = CoroutineStart.UNDISPATCHED) { + aapsLogger.info(LTag.EVENTS, "==>> ${clazz.simpleName} registered") + ab.set(true) + eventHashMap[clazz] = it + } } /** @@ -105,6 +106,8 @@ class RxHelper @Inject constructor( } fun clear() { - disposable.clear() + // Cancels the running collectors but keeps the scope usable, the way CompositeDisposable.clear() + // left its container usable. A plain scope.cancel() would make every later listen() do nothing. + scope.coroutineContext.cancelChildren() } } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt index 474242e5d7f9..ed43d5d25fc9 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt @@ -1,7 +1,6 @@ package app.aaps.core.interfaces.rx.bus import app.aaps.core.interfaces.rx.events.Event -import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.flow.Flow /** @@ -17,19 +16,14 @@ interface RxBus { fun send(event: Event) /** - * Subscribes to events of a specific type via RxJava Observable. + * Subscribes to events of a specific type. * - * @param eventType The class of the event to listen for. - * @return An [Observable] that emits events of the specified type. - */ - fun toObservable(eventType: Class): Observable - - /** - * Subscribes to events of a specific type via coroutines Flow. - * Use this in Compose/coroutine code instead of [toObservable]. + * The bus has no replay, so a collector only sees what is sent after it starts. Collect with + * `app.aaps.core.interfaces.rx.collectResilient` and `CoroutineStart.UNDISPATCHED` when the + * subscription is made from a constructor or `onStart`, so nothing sent right after it is lost. * * @param eventType The class of the event to listen for. * @return A [Flow] that emits events of the specified type. */ fun toFlow(eventType: Class): Flow -} \ No newline at end of file +} diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt index 6d9ed442d9f9..263162479dfe 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt @@ -11,7 +11,6 @@ import app.aaps.core.interfaces.rx.events.Event import app.aaps.core.interfaces.rx.events.EventShowDialog import app.aaps.core.ui.R import com.google.common.truth.Truth.assertThat -import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter @@ -142,8 +141,6 @@ class GlobalDialogHostTest { check(events.tryEmit(event)) { "event buffer overflow" } } - override fun toObservable(eventType: Class): Observable = - throw UnsupportedOperationException("not needed in tests") @Suppress("UNCHECKED_CAST") override fun toFlow(eventType: Class): Flow = diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt index 9c1d4ea8dc30..e4673e514d72 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt @@ -12,7 +12,6 @@ import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalPreferences -import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -74,8 +73,6 @@ class GlobalSnackbarHostTest { check(events.tryEmit(event)) { "event buffer overflow" } } - override fun toObservable(eventType: Class): Observable = - throw UnsupportedOperationException("not needed in tests") @Suppress("UNCHECKED_CAST") override fun toFlow(eventType: Class): Flow = diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt index 6fa0f1a79202..baff9bcf75db 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt @@ -9,8 +9,11 @@ import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.rx.events.EventProfileChangeRequested import app.aaps.core.interfaces.utils.DateUtil import app.aaps.shared.tests.TestBase -import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher @@ -41,14 +44,18 @@ class ProfileSwitchExpirySchedulerTest : TestBase() { private val now = 1_700_000_000_000L private var eventCount = 0 - private lateinit var disposable: Disposable + private lateinit var collector: Job @BeforeEach fun prepare() { whenever(dateUtil.now()).thenReturn(now) whenever(config.AAPSCLIENT).thenReturn(false) whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) - disposable = rxBus.toObservable(EventProfileChangeRequested::class.java).subscribe { eventCount++ } + // UNDISPATCHED so the collector is subscribed before the scheduler under test sends anything; + // RxBus has no replay, so a scheduled collector would miss those events. + collector = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { + rxBus.toFlow(EventProfileChangeRequested::class.java).collect { eventCount++ } + } scheduler = ProfileSwitchExpiryScheduler( persistenceLayer = persistenceLayer, rxBus = rxBus, @@ -61,7 +68,7 @@ class ProfileSwitchExpirySchedulerTest : TestBase() { @AfterEach fun tearDown() { - disposable.dispose() + collector.cancel() } @Test diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt index ba09ea44e4f9..7abc3b32a69b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt @@ -1,5 +1,4 @@ package app.aaps.plugins.sync.wear.wearintegration - import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.RM @@ -36,6 +35,11 @@ import app.aaps.core.objects.wizard.QuickWizardEntry import app.aaps.core.objects.wizard.QuickWizardMode import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -50,7 +54,6 @@ import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import org.mockito.kotlin.verifyBlocking import org.mockito.kotlin.whenever - /** * Tests for the unified wear **manual-bolus** ([DataHandlerMobile.handleBolusPreCheck]) and * **eCarbs** ([DataHandlerMobile.handleECarbsPreCheck]) precheck handlers. @@ -71,7 +74,6 @@ import org.mockito.kotlin.whenever * is captured off the real [RxBusImpl] (a synchronous `onNext`). */ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { - @Mock private lateinit var loop: Loop @Mock private lateinit var processedDeviceStatusData: ProcessedDeviceStatusData @Mock private lateinit var receiverStatusStore: ReceiverStatusStore @@ -88,9 +90,7 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { @Mock private lateinit var wizardExecutor: WizardExecutor @Mock private lateinit var pump: PumpWithConcentration @Mock private lateinit var automation: Automation - private lateinit var sut: DataHandlerMobile - @BeforeEach fun prepare() { sut = DataHandlerMobile( context, rxBus, aapsLogger, rh, preferences, config, @@ -107,41 +107,43 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { whenever(activePlugin.activePump).thenReturn(pump) whenever(pump.isInitialized()).thenReturn(true) } - + /** + * Collects the payloads the handler ships to the watch. UNDISPATCHED so the collector is + * subscribed before the block runs: RxBus has no replay, so a scheduled collector would miss + * everything the block sends. + */ + private fun collectMobileToWear(onPayload: (EventData) -> Unit): Job = + CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { + rxBus.toFlow(EventMobileToWear::class.java).collect { onPayload(it.payload) } + } /** Capture the single [ConfirmAction] the handler ships to the watch via [EventMobileToWear]. */ private inline fun capturedConfirm(block: () -> Unit): EventData.ConfirmAction { var captured: EventData? = null - val d = rxBus.toObservable(EventMobileToWear::class.java).subscribe { captured = it.payload } + val job = collectMobileToWear { captured = it } block() - d.dispose() + job.cancel() return captured as EventData.ConfirmAction } - /** Capture EVERY [EventMobileToWear] payload (e.g. a ContactingMaster spinner followed by the ConfirmAction). */ private inline fun captureAll(block: () -> Unit): List { val out = mutableListOf() - val d = rxBus.toObservable(EventMobileToWear::class.java).subscribe { out += it.payload } + val job = collectMobileToWear { out += it } block() - d.dispose() + job.cancel() return out } - // Bolus / eCarbs / TT / PS / RM all relay through the role-transparent BatchExecutor (master→local / client→round-trip). private suspend fun stubBatchPrepared(bolusId: Long, lines: List) = whenever(batchExecutor.prepare(any(), any(), any())).thenReturn(ActionProgress.Prepared(bolusId, lines)) - // QuickWizard + full Wizard relay through the role-transparent WizardExecutor. private suspend fun stubWizardPrepared(bolusId: Long, lines: List) = whenever(wizardExecutor.prepare(any(), any())).thenReturn(ActionProgress.Prepared(bolusId, lines)) - @Test fun `bolus precheck relays via BatchExecutor and ships bolusId plus master lines`() = runTest { stubBatchPrepared( bolusId = 4242L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.00 U"), ConfirmationLine(ConfirmationRole.CARBS, "Carbs: 10 g")) ) - val confirm = capturedConfirm { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 10)) } - // Role-transparent BatchExecutor is the single capping/parking authority — driven with a FIXED, immediate bolus. val captor = argumentCaptor>() verifyBlocking(batchExecutor) { prepare(captor.capture(), any(), any()) } @@ -153,7 +155,6 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { assertThat(bolus.recordOnly).isFalse() // Insulin no longer goes through the LOCAL executor — it relays. verifyBlocking(wizardBolusExecutor, never()) { prepareBatch(any()) } - // The round-trip carries only the parked bolusId + the master's lines — no echoed amount, no concatenated message. assertThat(confirm.returnCommand).isEqualTo(EventData.ActionBolusConfirmed(4242L)) assertThat(confirm.message).isEmpty() @@ -162,80 +163,58 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { EventData.ConfirmActionLine("CARBS", "Carbs: 10 g") ).inOrder() } - @Test fun `bolus precheck rejected by the relay becomes a sendError to the watch`() = runTest { whenever(batchExecutor.prepare(any(), any(), any())).thenReturn(ActionProgress.Rejected(FailureReason.ExecutionFailed, "boom")) - val sent = capturedConfirm { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 0)) } - assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) assertThat(sent.message).isEqualTo("boom") } - @Test fun `insulin bolus on a client relays to the master instead of a local refusal`() = runTest { whenever(config.AAPSCLIENT).thenReturn(true) // Master offline → the role-transparent relay returns NotReachable (the gate moved inside BatchExecutor). whenever(batchExecutor.prepare(any(), any(), any())).thenReturn(ActionProgress.Rejected(FailureReason.NotReachable)) - val sent = capturedConfirm { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 0)) } - // No blanket local refusal any more — the handler attempts the relay; it did NOT go to the local executor. verifyBlocking(batchExecutor) { prepare(any(), any(), any()) } verifyBlocking(wizardBolusExecutor, never()) { prepareBatch(any()) } assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) } - @Test fun `carbs-only bolus also relays through the role-transparent executor`() = runTest { whenever(config.AAPSCLIENT).thenReturn(true) stubBatchPrepared(7L, lines = listOf(ConfirmationLine(ConfirmationRole.CARBS, "Carbs: 10 g"))) - val confirm = capturedConfirm { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 0.0, carbs = 10)) } - verifyBlocking(batchExecutor) { prepare(any(), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionBolusConfirmed(7L)) } - // --- Watch-on-client feedback: ContactingMaster spinner + deferred confirm (no false success) --- - @Test fun `bolus precheck on a client emits ContactingMaster and defers the confirm`() = runTest { whenever(config.AAPSCLIENT).thenReturn(true) stubBatchPrepared(1L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.0 U"))) - val events = captureAll { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 0)) } - // Spinner emitted before the round-trip; the confirm is deferred so the watch won't flash a false success. assertThat(events.any { it is EventData.ContactingMaster }).isTrue() assertThat(events.filterIsInstance().single().deferConfirm).isTrue() } - @Test fun `bolus precheck on a master neither contacts nor defers`() = runTest { // config.AAPSCLIENT defaults to false → local instant prepare, success shown locally on the watch. stubBatchPrepared(1L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.0 U"))) - val events = captureAll { sut.handleBolusPreCheck(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 0)) } - assertThat(events.none { it is EventData.ContactingMaster }).isTrue() assertThat(events.filterIsInstance().single().deferConfirm).isFalse() } - @Test fun `bolus confirm relays the parked id through BatchExecutor commit`() { // The confirm runs through the async onEvent chain → timeout-verify (mirrors the other wiring tests). // (Applied → RemoteDelivered to the watch is onCommitResult logic, gated on AAPSCLIENT; not captured here.) runBlocking { whenever(batchExecutor.commit(any(), any(), any(), any())).thenReturn(ActionProgress.Applied) } - rxBus.send(EventData.ActionBolusConfirmed(5L)) - verifyBlocking(batchExecutor, timeout(2000)) { commit(eq(5L), any(), any(), any()) } } - @Test fun `ecarbs precheck maps to a fixed carbs-only bolus carrying offset plus duration with an ECarbs confirm`() = runTest { stubBatchPrepared( 99L, lines = listOf(ConfirmationLine(ConfirmationRole.CARBS, "Carbs: 20 g"), ConfirmationLine(ConfirmationRole.NORMAL, "Duration: 3 h")) ) - val confirm = capturedConfirm { sut.handleECarbsPreCheck(EventData.ActionECarbsPreCheck(carbs = 20, carbsTimeShift = 15, duration = 3)) } - val captor = argumentCaptor>() verifyBlocking(batchExecutor) { prepare(captor.capture(), any(), any()) } val bolus = captor.firstValue.single() as BatchAction.Bolus @@ -243,130 +222,99 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { assertThat(bolus.carbs).isEqualTo(20) assertThat(bolus.carbsTimeOffsetMinutes).isEqualTo(15) assertThat(bolus.carbsDurationHours).isEqualTo(3) - assertThat(confirm.returnCommand).isEqualTo(EventData.ActionECarbsConfirmed(99L)) assertThat(confirm.lines).containsExactly( EventData.ConfirmActionLine("CARBS", "Carbs: 20 g"), EventData.ConfirmActionLine("NORMAL", "Duration: 3 h") ).inOrder() } - // --- Temp target (fully unified onto the shared prepareBatch/confirm + applyTempTarget path) ----------- - @Test fun `manual temp target relays a TempTarget batch and ships its bolusId + master lines`() = runTest { whenever(profileFunction.getUnits()).thenReturn(GlucoseUnit.MGDL) stubBatchPrepared(555L, lines = listOf(ConfirmationLine(ConfirmationRole.NORMAL, "Temporary target: 100 – 120 mg/dl (30 mins)"))) - val confirm = capturedConfirm { sut.handleTempTargetPreCheck( EventData.ActionTempTargetPreCheck(EventData.ActionTempTargetPreCheck.TempTargetCommand.MANUAL, isMgdl = true, duration = 30, low = 100.0, high = 120.0) ) } - // Routed through the role-transparent BatchExecutor (mg/dL range, reason WEAR) → the MASTER applies it via // applyTempTarget (local on master, BatchPrepare round-trip on a client); the wear ✓ commits by bolusId. verifyBlocking(batchExecutor) { prepare(eq(listOf(BatchAction.TempTarget(TT.Reason.WEAR.text, 100.0, 120.0, 30, 0))), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionTempTargetConfirmed(555L)) assertThat(confirm.lines).containsExactly(EventData.ConfirmActionLine("NORMAL", "Temporary target: 100 – 120 mg/dl (30 mins)")) } - @Test fun `cancel temp target relays a zero-duration TempTarget batch`() = runTest { whenever(profileFunction.getUnits()).thenReturn(GlucoseUnit.MGDL) stubBatchPrepared(7L, lines = listOf(ConfirmationLine(ConfirmationRole.NORMAL, "Temporary target: Cancel"))) - val confirm = capturedConfirm { sut.handleTempTargetPreCheck(EventData.ActionTempTargetPreCheck(EventData.ActionTempTargetPreCheck.TempTargetCommand.CANCEL)) } - verifyBlocking(batchExecutor) { prepare(eq(listOf(BatchAction.TempTarget(TT.Reason.WEAR.text, 0.0, 0.0, 0, 0))), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionTempTargetConfirmed(7L)) } - @Test fun `manual temp target with mismatched units sends an error, not a relay`() = runTest { whenever(profileFunction.getUnits()).thenReturn(GlucoseUnit.MGDL) // profile mg/dL but the action says mmol - val sent = capturedConfirm { sut.handleTempTargetPreCheck( EventData.ActionTempTargetPreCheck(EventData.ActionTempTargetPreCheck.TempTargetCommand.MANUAL, isMgdl = false, duration = 30, low = 5.5, high = 6.5) ) } - verifyBlocking(batchExecutor, never()) { prepare(any(), any(), any()) } assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) } - // --- Profile switch (unified onto the shared prepareBatch/confirm + applyProfileSwitch path) ----------- - @Test fun `profile switch precheck relays a ProfileSwitch batch and ships its bolusId + master lines`() = runTest { stubBatchPrepared(321L, lines = listOf(ConfirmationLine(ConfirmationRole.PRIMARY, "Profile: Test"))) - val confirm = capturedConfirm { sut.handleProfileSwitchPreCheck(EventData.ActionProfileSwitchPreCheck(timeShift = 2, percentage = 120, duration = 60)) } - // The wear command maps to a single ProfileSwitch batch action; the MASTER validates/parks and the ✓ commits by bolusId. verifyBlocking(batchExecutor) { prepare(eq(listOf(BatchAction.ProfileSwitch(120, 2, 60))), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionProfileSwitchConfirmed(321L)) assertThat(confirm.lines).containsExactly(EventData.ConfirmActionLine("PRIMARY", "Profile: Test")) } - @Test fun `profile switch precheck rejected by the relay becomes a sendError to the watch`() = runTest { whenever(batchExecutor.prepare(any(), any(), any())).thenReturn(ActionProgress.Rejected(FailureReason.ExecutionFailed, "no profile")) - val sent = capturedConfirm { sut.handleProfileSwitchPreCheck(EventData.ActionProfileSwitchPreCheck(timeShift = 0, percentage = 100, duration = 30)) } - assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) assertThat(sent.message).isEqualTo("no profile") } - // --- Running mode (unified onto the shared negotiate → prepareBatch/confirm + applyRunningMode path) --- - /** Negotiate the available modes and return the master-issued nonce + the wear-tile list (to pick an index). */ private suspend fun negotiateRunningModes(allowed: List): EventData.RunningModeList { whenever(pump.pumpDescription).thenReturn(PumpDescription()) whenever(profileFunction.isProfileValid(any())).thenReturn(true) whenever(loop.allowedNextModes()).thenReturn(allowed) var list: EventData.RunningModeList? = null - val d = rxBus.toObservable(EventMobileToWear::class.java).subscribe { (it.payload as? EventData.RunningModeList)?.let { l -> list = l } } + val job = collectMobileToWear { (it as? EventData.RunningModeList)?.let { l -> list = l } } sut.handleAvailableRunningModes() - d.dispose() + job.cancel() return list!! } - @Test fun `running mode selected relays a RunningMode batch and ships its bolusId + master lines`() = runTest { val list = negotiateRunningModes(listOf(RM.Mode.CLOSED_LOOP, RM.Mode.OPEN_LOOP)) val idx = list.states.indexOfFirst { it.state == AvailableRunningMode.RunningMode.LOOP_CLOSED } stubBatchPrepared(555L, lines = listOf(ConfirmationLine(ConfirmationRole.PRIMARY, "Running mode: Closed Loop"))) - val confirm = capturedConfirm { sut.handleRunningModeSelected(EventData.RunningModeSelected(list.timeStamp, idx, null)) } - // The selected tile maps to a single RunningMode batch action; the MASTER re-validates/parks and the ✓ commits by bolusId. verifyBlocking(batchExecutor) { prepare(eq(listOf(BatchAction.RunningMode(RM.Mode.CLOSED_LOOP, 0))), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.RunningModeConfirmed(555L)) assertThat(confirm.lines).containsExactly(EventData.ConfirmActionLine("PRIMARY", "Running mode: Closed Loop")) } - @Test fun `running mode selected with a stale nonce is rejected before the relay`() = runTest { negotiateRunningModes(listOf(RM.Mode.CLOSED_LOOP)) - val sent = capturedConfirm { sut.handleRunningModeSelected(EventData.RunningModeSelected(timeStamp = 1L, index = 0, duration = null)) } - assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) verifyBlocking(batchExecutor, never()) { prepare(any(), any(), any()) } } - @Test fun `running mode confirmed commits the parked bolusId via the relay`() = runTest { // The post-confirm tile refresh short-circuits when no profile is valid (no pumpDescription stub needed). whenever(batchExecutor.commit(any(), any(), any(), any())).thenReturn(ActionProgress.Applied) whenever(profileFunction.isProfileValid(any())).thenReturn(false) - sut.handleRunningModeConfirmed(EventData.RunningModeConfirmed(777L)) - verifyBlocking(batchExecutor) { commit(eq(777L), eq(Sources.Wear), any(), any()) } } - // --- Full wizard (unified onto the shared prepareWizard/confirm + master-authored lines) --------------- - @Test fun `wizard precheck relays via WizardExecutor and ships its bolusId + master lines`() = runTest { // The full watch wizard recomputes through the role-transparent WizardExecutor (master→local prepareWizard / // client→WizardPrepare round-trip), rendering the master-authored lines (no bespoke WizardResultActivity). @@ -376,31 +324,24 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { whenever(profileFunction.getUnits()).thenReturn(GlucoseUnit.MGDL) whenever(preferences.get(any())).thenReturn(true) // WearWizard* toggles stubWizardPrepared(555L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.50 U"))) - val confirm = capturedConfirm { sut.handleWizardPreCheck(EventData.ActionWizardPreCheck(carbs = 30, percentage = 100)) } - verifyBlocking(wizardExecutor) { prepare(any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionWizardConfirmed(555L)) assertThat(confirm.lines).containsExactly(EventData.ConfirmActionLine("BOLUS", "Bolus: 1.50 U")) } - @Test fun `wizard precheck with no master BG sends an error without reaching the relay`() = runTest { val ads = mock() whenever(iobCobCalculator.ads).thenReturn(ads) whenever(ads.actualBg()).thenReturn(null) - val sent = capturedConfirm { sut.handleWizardPreCheck(EventData.ActionWizardPreCheck(carbs = 0, percentage = 100)) } - assertThat(sent.returnCommand).isInstanceOf(EventData.Error::class.java) verifyBlocking(wizardExecutor, never()) { prepare(any(), any()) } } - // --- QuickWizard tile modes (branch on entry.mode() exactly like the phone's MainViewModel) ----------- // // The wear QuickWizard handler used to ALWAYS recompute via WizardExecutor, so a fixed INSULIN button // computed a wizard dose and a CARBS-only button could deliver correction insulin. It now branches: // INSULIN/CARBS → fixed BatchExecutor bolus (ActionBolusConfirmed); WIZARD → recompute (ActionWizardConfirmed). - /** A mocked synced [QuickWizardEntry] resolved by [guid], with the given mode + fixed amounts. */ private fun stubQuickWizard(guid: String, mode: QuickWizardMode, insulin: Double = 0.0, carbs: Int = 0, text: String = "QW"): QuickWizardEntry { val entry = mock() @@ -411,13 +352,10 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { whenever(quickWizard.get(guid)).thenReturn(entry) return entry } - @Test fun `quick wizard INSULIN mode relays a fixed insulin batch with an ActionBolusConfirmed`() = runTest { stubQuickWizard("g1", QuickWizardMode.INSULIN, insulin = 1.5, text = "Bolus") stubBatchPrepared(11L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.50 U"))) - val confirm = capturedConfirm { sut.handleQuickWizardPreCheck(EventData.ActionQuickWizardPreCheck("g1")) } - val captor = argumentCaptor>() verifyBlocking(batchExecutor) { prepare(captor.capture(), any(), any()) } val bolus = captor.firstValue.single() as BatchAction.Bolus @@ -428,13 +366,10 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { verifyBlocking(wizardExecutor, never()) { prepare(any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionBolusConfirmed(11L)) } - @Test fun `quick wizard CARBS mode relays a fixed carbs batch with an ActionBolusConfirmed`() = runTest { stubQuickWizard("g2", QuickWizardMode.CARBS, carbs = 20, text = "Carbs") stubBatchPrepared(12L, lines = listOf(ConfirmationLine(ConfirmationRole.CARBS, "Carbs: 20 g"))) - val confirm = capturedConfirm { sut.handleQuickWizardPreCheck(EventData.ActionQuickWizardPreCheck("g2")) } - val captor = argumentCaptor>() verifyBlocking(batchExecutor) { prepare(captor.capture(), any(), any()) } val bolus = captor.firstValue.single() as BatchAction.Bolus @@ -445,31 +380,24 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { verifyBlocking(wizardExecutor, never()) { prepare(any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionBolusConfirmed(12L)) } - @Test fun `quick wizard WIZARD mode recomputes via WizardExecutor with an ActionWizardConfirmed`() = runTest { stubQuickWizard("g3", QuickWizardMode.WIZARD, carbs = 30, text = "Wizard") stubWizardPrepared(13L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 2.00 U"))) - val confirm = capturedConfirm { sut.handleQuickWizardPreCheck(EventData.ActionQuickWizardPreCheck("g3")) } - verifyBlocking(wizardExecutor) { prepare(any(), any()) } verifyBlocking(batchExecutor, never()) { prepare(any(), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionWizardConfirmed(13L)) } - @Test fun `quick wizard with an unknown guid falls through to the wizard path for a proper not-available error`() = runTest { // quickWizard.get(guid) returns null (entry deleted/unsynced) → no mode to branch on; route to WizardExecutor, // which ships a "quick wizard not available" error rather than silently dropping the tap. whenever(quickWizard.get("gone")).thenReturn(null) stubWizardPrepared(99L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.00 U"))) - val confirm = capturedConfirm { sut.handleQuickWizardPreCheck(EventData.ActionQuickWizardPreCheck("gone")) } - verifyBlocking(wizardExecutor) { prepare(any(), any()) } verifyBlocking(batchExecutor, never()) { prepare(any(), any(), any()) } assertThat(confirm.returnCommand).isEqualTo(EventData.ActionWizardConfirmed(99L)) } - @Test fun `quick wizard fixed-mode tags the batch with the guid and never marks the entry locally`() = runTest { // SOT: the guid travels in the batch so the MASTER marks the entry used in confirm(); this device must NOT write // the synced QuickWizard pref itself (on a client that pushes it back over the round-trip → "Update settings … @@ -477,23 +405,19 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { val entry = stubQuickWizard("g4", QuickWizardMode.INSULIN, insulin = 1.0, text = "Bolus") stubBatchPrepared(14L, lines = listOf(ConfirmationLine(ConfirmationRole.BOLUS, "Bolus: 1.00 U"))) whenever(batchExecutor.commit(any(), any(), any(), any())).thenReturn(ActionProgress.Applied) - val captor = argumentCaptor>() capturedConfirm { sut.handleQuickWizardPreCheck(EventData.ActionQuickWizardPreCheck("g4")) } verifyBlocking(batchExecutor) { prepare(captor.capture(), any(), any()) } rxBus.send(EventData.ActionBolusConfirmed(14L)) verifyBlocking(batchExecutor, timeout(2000)) { commit(eq(14L), any(), any(), any()) } // the commit handler ran - assertThat((captor.firstValue.single() as BatchAction.Bolus).quickWizardGuid).isEqualTo("g4") verify(entry, never()).markAsUsed() // …and it did NOT mark locally — the master does, via the guid } - // --- Subscription wiring (the onEvent / onEventSync helpers actually register each type) -------------- // // The tests above drive the handler methods directly. These two instead post the event onto the real // RxBus and assert the handler runs — locking in that the helper-based subscriptions in init {} are // wired (a dropped subscription is a mechanical refactor error the compiler can't catch). - @Test fun `onEventSync dispatches a posted SnoozeAlert to its handler`() { // Was a bare verify: the test's trampoline io scheduler ran the Rx subscription inline on the // posting thread. onEventSync collects a Flow on Dispatchers.IO now, so the handler runs off @@ -502,22 +426,17 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { // Delivery itself is not racy: the collector subscribes UNDISPATCHED, so it is registered // before send() is reached. rxBus.send(EventData.SnoozeAlert(0L)) - verify(uiInteraction, timeout(2000)).stopAlarm("Muted from wear") } - @Test fun `onEvent dispatches a posted ActionBolusPreCheck to the suspend handler`() { // onEvent wraps the handler in rxCompletable, which runs the coroutine off the posting thread — // hence a timeout verify rather than a synchronous capture. The unstubbed prepare() returns the // mockito default; we only assert it was reached. rxBus.send(EventData.ActionBolusPreCheck(insulin = 1.0, carbs = 10)) - verifyBlocking(batchExecutor, timeout(2000)) { prepare(any(), any(), any()) } } - @Test fun `onEvent dispatches a posted ActionProfileSwitchPreCheck to the handler`() { rxBus.send(EventData.ActionProfileSwitchPreCheck(timeShift = 0, percentage = 110, duration = 30)) - verifyBlocking(batchExecutor, timeout(2000)) { prepare(any(), any(), any()) } } } diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/di/SharedImplModule.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/di/SharedImplModule.kt index 164afdf400ba..ac0a09e2df13 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/di/SharedImplModule.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/di/SharedImplModule.kt @@ -42,7 +42,7 @@ open class SharedImplModule { @Provides @Singleton - fun provideRxBus(aapsSchedulers: AapsSchedulers, aapsLogger: AAPSLogger): RxBus = RxBusImpl(aapsSchedulers, aapsLogger) + fun provideRxBus(aapsLogger: AAPSLogger): RxBus = RxBusImpl(aapsLogger) @Provides @Singleton diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt index b2c2d4b1c41a..1ee04d713de0 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt @@ -2,12 +2,9 @@ package app.aaps.shared.impl.rx.bus import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.Event import app.aaps.core.interfaces.rx.events.EventUpdateOverviewCalcProgress -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.subjects.PublishSubject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -18,11 +15,9 @@ import javax.inject.Singleton @Singleton class RxBusImpl @Inject constructor( - val aapsSchedulers: AapsSchedulers, val aapsLogger: AAPSLogger ) : RxBus { - private val publisher = PublishSubject.create() private val flowPublisher = MutableSharedFlow( extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST @@ -31,20 +26,12 @@ class RxBusImpl @Inject constructor( override fun send(event: Event) { if (event !is EventUpdateOverviewCalcProgress) aapsLogger.debug(LTag.EVENTS, "Sending $event") - publisher.onNext(event) flowPublisher.tryEmit(event) } - // Listen should return an Observable and not the publisher - // Using ofType we filter only events that match that class type - override fun toObservable(eventType: Class): Observable = - publisher - .subscribeOn(aapsSchedulers.io) - .ofType(eventType) - @Suppress("UNCHECKED_CAST") override fun toFlow(eventType: Class): Flow = flowPublisher .filter { eventType.isInstance(it) } .map { it as T } -} \ No newline at end of file +} diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBase.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBase.kt index a55bed4dda01..b92acdea61b2 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBase.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBase.kt @@ -27,7 +27,7 @@ open class TestBase { MockitoAnnotations.openMocks(this) Locale.setDefault(Locale.ENGLISH) System.setProperty("disableFirebase", "true") - rxBus = RxBusImpl(aapsSchedulers, aapsLogger) + rxBus = RxBusImpl(aapsLogger) } @AfterEach diff --git a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt index 3a4369870dce..fed2af09862b 100644 --- a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt @@ -15,6 +15,10 @@ import app.aaps.wear.data.ComplicationDataRepository import com.google.common.truth.Truth.assertThat import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import org.junit.jupiter.api.BeforeEach @@ -43,7 +47,7 @@ internal class DataHandlerWearTest : WearTestBase() { @BeforeEach fun setupHandler() { whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) - rxBus = RxBusImpl(aapsSchedulers, logger) + rxBus = RxBusImpl(logger) sut = DataHandlerWear(context, rxBus, sp, preferences, logger, complicationDataRepository) } @@ -67,8 +71,11 @@ internal class DataHandlerWearTest : WearTestBase() { @Test fun `a ping is answered with a pong to the mobile`() { val pong = CompletableDeferred() - rxBus.toObservable(EventWearToMobile::class.java).subscribe { evt -> - (evt.payload as? EventData.ActionPong)?.let { pong.complete(it) } + // UNDISPATCHED so the collector is subscribed before the ping is sent; RxBus has no replay. + val collector = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { + rxBus.toFlow(EventWearToMobile::class.java).collect { evt -> + (evt.payload as? EventData.ActionPong)?.let { pong.complete(it) } + } } rxBus.send(EventData.ActionPing(1_000L)) @@ -76,6 +83,7 @@ internal class DataHandlerWearTest : WearTestBase() { // Answered from the handler's IO dispatcher, so wait for it rather than reading a field. val answer = runBlocking { withTimeoutOrNull(HANDLER_TIMEOUT_MS) { pong.await() } } assertThat(answer).isNotNull() + collector.cancel() } companion object { From 4d830574982693344b9799da0849f165aa158513 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 18:28:58 +0200 Subject: [PATCH 097/146] Clean up Rx leftovers from the Flow migration Removes what the converted call sites no longer need: - plugins/sync: the kotlinx-coroutines-rx3 dependency, its last user was the heart-rate/steps rxCompletable that is now a Flow collector. - DanaRSOverviewViewModelTest and DataHandlerWearTest: the AapsSchedulers mock and its trampoline stub, dead since the view model and RxBusImpl stopped taking one. - DataHandlerWearTest: the class doc still described trampoline delivery. --- plugins/sync/build.gradle.kts | 1 - .../pump/danars/compose/DanaRSOverviewViewModelTest.kt | 4 ---- .../test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt | 8 ++------ 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index 60f89547d527..fc5742563122 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -35,7 +35,6 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) - implementation(libs.kotlinx.coroutines.rx3) implementation(libs.kotlinx.datetime) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.androidx.work.testing) diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt index 684e279f6ca2..0d57dee3119a 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt @@ -15,7 +15,6 @@ import app.aaps.core.interfaces.pump.PumpWithConcentration import app.aaps.core.interfaces.pump.ble.BleTransport import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventInitializationChanged import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged @@ -31,7 +30,6 @@ import app.aaps.pump.dana.keys.DanaStringNonKey import app.aaps.pump.danars.DanaRSPlugin import com.google.common.truth.Truth.assertThat import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -63,7 +61,6 @@ internal class DanaRSOverviewViewModelTest { @Mock private lateinit var aapsLogger: AAPSLogger @Mock private lateinit var rh: ResourceHelper @Mock private lateinit var rxBus: RxBus - @Mock private lateinit var aapsSchedulers: AapsSchedulers @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var dateUtil: DateUtil @Mock private lateinit var activePlugin: ActivePlugin @@ -96,7 +93,6 @@ internal class DanaRSOverviewViewModelTest { whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) - whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) diff --git a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt index fed2af09862b..d3cc36ddc2c5 100644 --- a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt @@ -1,6 +1,5 @@ package app.aaps.wear.comm -import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData @@ -13,7 +12,6 @@ import app.aaps.wear.R import app.aaps.wear.WearTestBase import app.aaps.wear.data.ComplicationDataRepository import com.google.common.truth.Truth.assertThat -import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -29,8 +27,8 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever /** - * Drives [DataHandlerWear]'s message handlers through a REAL [RxBusImpl] (trampoline scheduler, so - * delivery is synchronous). The `Preferences` handler writes to sp/preferences unconditionally — only + * Drives [DataHandlerWear]'s message handlers through a REAL [RxBusImpl]. Handlers run on the + * collector's IO dispatcher, so the assertions wait. The `Preferences` handler writes to sp/preferences unconditionally — only * the tile-refresh side runs when `wearControl` changes, so sending it with an unchanged wearControl * keeps the handler Android-free and assertable. Construction touches no Android, so no Robolectric. */ @@ -38,7 +36,6 @@ internal class DataHandlerWearTest : WearTestBase() { @Mock lateinit var preferences: Preferences @Mock lateinit var complicationDataRepository: ComplicationDataRepository - @Mock lateinit var aapsSchedulers: AapsSchedulers private val logger = AAPSLoggerTest() private lateinit var rxBus: RxBus @@ -46,7 +43,6 @@ internal class DataHandlerWearTest : WearTestBase() { @BeforeEach fun setupHandler() { - whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) rxBus = RxBusImpl(logger) sut = DataHandlerWear(context, rxBus, sp, preferences, logger, complicationDataRepository) } From 48218de0ea71c87daa0f9200fd33f07f33be3725 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 18:36:30 +0200 Subject: [PATCH 098/146] SWItem, NSClientV3Service: drop RxJava Both are wanted on other platforms, so RxJava has to go. SWItem.scheduleChange debounced its EventSWUpdate with Completable.timer and a Disposable that only the next call ever cancelled. That is now a Job and delay() on a scope with the same lifetime, which also removes the java.util.concurrent TimeUnit import. With those gone the file only depends on Compose and one StringRes annotation on the label(Int) convenience overload. NSClientV3Service just declared a CompositeDisposable and cleared it in onDestroy. Nothing was ever added to it, so it is deleted with no change in behaviour. The service itself stays Android only: it is a DaggerService with a Binder and a wakelock. :plugins:configuration has no RxJava user left at all, so its rxandroid dependency goes too. --- plugins/configuration/build.gradle.kts | 1 - .../setupwizard/elements/SWItem.kt | 26 ++++++++++++------- .../nsclientV3/services/NSClientV3Service.kt | 3 --- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index 4fa523ae8810..1d5f570d7e3e 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -40,7 +40,6 @@ dependencies { api(libs.androidx.lifecycle.runtime.compose) implementation(libs.com.google.dagger.hilt.android) - implementation(libs.io.reactivex.rxjava3.rxandroid) ksp(libs.com.google.dagger.compiler) ksp(libs.com.google.dagger.hilt.compiler) diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt index 20c9c8ec2267..16b8abeda5d5 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/elements/SWItem.kt @@ -12,10 +12,14 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.StringNonPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.TextRef -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.disposables.Disposable -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds open class SWItem @Inject constructor( val aapsLogger: AAPSLogger, @@ -25,7 +29,10 @@ open class SWItem @Inject constructor( val passwordCheck: PasswordCheck ) { - private var scheduledEventPost: Disposable? = null + // Lives as long as the item, like the Disposable before it, which only the next scheduleChange + // ever cancelled. Default because the body only sends on the bus and touches no UI. + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private var scheduledEventPost: Job? = null var label: TextRef? = null var comment: Int? = null @@ -58,11 +65,10 @@ open class SWItem @Inject constructor( fun scheduleChange(updateDelay: Long) { // cancel waiting task to prevent sending multiple posts - scheduledEventPost?.dispose() - scheduledEventPost = Completable - .timer(updateDelay, TimeUnit.SECONDS) - .subscribe { - rxBus.send(EventSWUpdate(false)) - } + scheduledEventPost?.cancel() + scheduledEventPost = scope.launch { + delay(updateDelay.seconds) + rxBus.send(EventSWUpdate(false)) + } } } \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index 0400c94e1a0c..bae4ffc07f52 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -42,7 +42,6 @@ import app.aaps.plugins.sync.nsclientV3.extensions.toRunningConfiguration import app.aaps.plugins.sync.nsclientV3.json.JsonBridge.toKotlinxJson import app.aaps.plugins.sync.nsclientV3.keys.NsclientBooleanKey import dagger.android.DaggerService -import io.reactivex.rxjava3.disposables.CompositeDisposable import io.socket.client.Ack import io.socket.client.IO import io.socket.client.Socket @@ -72,7 +71,6 @@ class NSClientV3Service : DaggerService() { @Inject lateinit var orphanDetector: OrphanDetector @Inject @ApplicationScope lateinit var appScope: CoroutineScope - private val disposable = CompositeDisposable() private var wakeLock: PowerManager.WakeLock? = null private val binder: IBinder = LocalBinder(this) @@ -88,7 +86,6 @@ class NSClientV3Service : DaggerService() { override fun onDestroy() { super.onDestroy() shutdownWebsockets() - disposable.clear() if (wakeLock?.isHeld == true) wakeLock?.release() } From af715a661788df811efae7499687c2597870aea8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 20:55:49 +0200 Subject: [PATCH 099/146] ResourceHelper: drop the members nobody calls The Compose migration took over drawables, dimensions and array/bool resources, but the ResourceHelper methods for them stayed. A search over the whole project finds no caller for gd, gb, gcs, gsa, getDisplayMetrics or dpToPx (both overloads), so they go. That also removes Drawable, DisplayMetrics, ArrayRes, BoolRes and DrawableRes from the interface. What is left needing an Android type is small: Bitmap in decodeResource (4 callers), AssetFileDescriptor in openRawResourceFd (2) and the ColorRes of gc (6). gb had one caller after all: shortTextMode() inside the implementation, which now reads the bool resource directly. The fake ResourceHelper in DateUtilImplTest loses the same overrides. This is groundwork for KMP. ResourceHelper is the single biggest blocker in :core:interfaces - moving all androidMain files to commonMain and compiling for iosArm64 gives 24 errors that name it, more than any other symbol. --- .../interfaces/resources/ResourceHelper.kt | 12 ------- .../resources/ResourceHelperImpl.kt | 35 ++----------------- .../shared/impl/utils/DateUtilImplTest.kt | 9 ----- 3 files changed, 3 insertions(+), 53 deletions(-) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index 70a8be68a55c..234b3b26bf73 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -2,13 +2,8 @@ package app.aaps.core.interfaces.resources import android.content.res.AssetFileDescriptor import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.util.DisplayMetrics -import androidx.annotation.ArrayRes -import androidx.annotation.BoolRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.RawRes import androidx.annotation.StringRes @@ -63,16 +58,9 @@ interface ResourceHelper { } @ColorInt fun gc(@ColorRes id: Int): Int - fun gd(@DrawableRes id: Int): Drawable? - fun gb(@BoolRes id: Int): Boolean - fun gcs(@ColorRes id: Int): String - fun gsa(@ArrayRes id: Int): Array fun openRawResourceFd(@RawRes id: Int): AssetFileDescriptor? fun decodeResource(id: Int): Bitmap - fun getDisplayMetrics(): DisplayMetrics - fun dpToPx(dp: Int): Int - fun dpToPx(dp: Float): Int fun shortTextMode(): Boolean } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt index 2f35bbe8f0f9..329465b89a4d 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt @@ -1,20 +1,13 @@ package app.aaps.implementation.resources -import android.annotation.SuppressLint import android.content.Context import android.content.res.AssetFileDescriptor import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.graphics.drawable.Drawable -import android.util.DisplayMetrics -import androidx.annotation.ArrayRes -import androidx.annotation.BoolRes import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.fabric.FabricPrivacy @@ -91,35 +84,13 @@ class ResourceHelperImpl @Inject constructor(var context: Context, private val f override fun gc(@ColorRes id: Int): Int = ContextCompat.getColor(context, id) - override fun gd(@DrawableRes id: Int): Drawable? = AppCompatResources.getDrawable(context, id) - - override fun gb(@BoolRes id: Int): Boolean = context.resources.getBoolean(id) - - @SuppressLint("ResourceType") - override fun gcs(@ColorRes id: Int): String = - gs(id).replace("#ff", "#") - - override fun gsa(@ArrayRes id: Int): Array = - context.resources.getStringArray(id) - override fun openRawResourceFd(id: Int): AssetFileDescriptor = context.resources.openRawResourceFd(id) override fun decodeResource(id: Int): Bitmap = BitmapFactory.decodeResource(context.resources, id) - override fun getDisplayMetrics(): DisplayMetrics = - context.resources.displayMetrics - - override fun dpToPx(dp: Int): Int { - val scale = context.resources.displayMetrics.density - return (dp * scale + 0.5f).toInt() - } - - override fun dpToPx(dp: Float): Int { - val scale = context.resources.displayMetrics.density - return (dp * scale + 0.5f).toInt() - } - - override fun shortTextMode(): Boolean = !gb(app.aaps.core.ui.R.bool.isTablet) + // Reads the bool resource directly. It used to go through gb(), which was dropped from the + // interface because this was its only caller. + override fun shortTextMode(): Boolean = !context.resources.getBoolean(app.aaps.core.ui.R.bool.isTablet) } diff --git a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt index 7c9ce5cdf24f..ce58f4cd2699 100644 --- a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt +++ b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt @@ -3,9 +3,7 @@ package app.aaps.shared.impl.utils import android.content.Context import android.content.res.AssetFileDescriptor import android.graphics.Bitmap -import android.graphics.drawable.Drawable import android.text.format.DateFormat -import android.util.DisplayMetrics import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import org.joda.time.DateTimeZone @@ -1529,15 +1527,8 @@ class DateUtilImplTest { override fun gq(id: Int, quantity: Int, vararg args: Any?): String = "" override fun gsNotLocalised(id: Int, vararg args: Any?): String = "" override fun gc(id: Int): Int = 0 - override fun gd(id: Int): Drawable? = null - override fun gb(id: Int): Boolean = false - override fun gcs(id: Int): String = "" - override fun gsa(id: Int): Array = emptyArray() override fun openRawResourceFd(id: Int): AssetFileDescriptor? = null override fun decodeResource(id: Int): Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - override fun getDisplayMetrics(): DisplayMetrics = DisplayMetrics() - override fun dpToPx(dp: Int): Int = dp - override fun dpToPx(dp: Float): Int = dp.toInt() override fun shortTextMode(): Boolean = true } } From 5ab8f8e9ffee15d61abcc8a072e72a1e70c16c9f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 21:13:39 +0200 Subject: [PATCH 100/146] ResourceHelper: move the last non-string members to their call sites gc, decodeResource and openRawResourceFd were the only members left that needed an Android type. Twelve call sites in total, so they move to the callers instead of staying on an interface every module depends on. decodeResource (4 callers) and openRawResourceFd (2) are notification and alarm sound code that already has a Context injected. AlarmSoundPlayerImpl shows the wrapper was not even used consistently: one line called rh.openRawResourceFd, a few lines down another called context.resources.openRawResourceFd directly. They all read from context.resources now. gc had 6 callers, all in the two Glance widget state loaders, and neither of them has a Context. Injecting one to read widget_low, widget_inrange and widget_high would add a dependency for nothing: those colors have no values-night variant, so the lookup could only ever return one fixed value. They become BgGraphColors.WIDGET constants with the same ARGB values. The colors stay deliberately separate from the AapsTheme BG colors. A widget sits on the launcher over unknown wallpaper, so it keeps one high-contrast set rather than following the app light and dark themes - theme bgHigh is orange in light mode, the widget stays yellow. The color resources themselves stay, small_widget_layout.xml still uses widget_inrange. ResourceHelper is now gs, gq, gsNotLocalised and shortTextMode, and the only Android imports left are the StringRes and PluralsRes annotations. What blocks it from commonMain is now only the Int resource id, which is the TextRef question. --- .../interfaces/resources/ResourceHelper.kt | 8 ------- .../AlarmNotificationManager.kt | 3 ++- .../AlarmSoundPlayerImpl.kt | 2 +- .../NotificationHolderImpl.kt | 3 ++- .../notifications/NotificationManagerImpl.kt | 3 ++- .../resources/ResourceHelperImpl.kt | 13 ------------ .../PersistentNotificationPlugin.kt | 3 ++- .../shared/impl/utils/DateUtilImplTest.kt | 5 ----- .../ui/widget/glance/BgGraphBitmapRenderer.kt | 21 ++++++++++++++++++- .../ui/widget/glance/BgGraphStateLoader.kt | 6 +----- .../ui/widget/glance/WidgetStateLoader.kt | 6 +++--- 11 files changed, 33 insertions(+), 40 deletions(-) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index 234b3b26bf73..fd285dfacccd 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -1,11 +1,6 @@ package app.aaps.core.interfaces.resources -import android.content.res.AssetFileDescriptor -import android.graphics.Bitmap -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes import androidx.annotation.PluralsRes -import androidx.annotation.RawRes import androidx.annotation.StringRes import app.aaps.core.interfaces.InterfacesStringIds import app.aaps.core.keys.KeysStringIds @@ -57,10 +52,7 @@ interface ResourceHelper { ?: ref.name } - @ColorInt fun gc(@ColorRes id: Int): Int - fun openRawResourceFd(@RawRes id: Int): AssetFileDescriptor? - fun decodeResource(id: Int): Bitmap fun shortTextMode(): Boolean } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt index 52d72aeedcd8..225139d4c5f7 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt @@ -7,6 +7,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import android.media.AudioAttributes import android.net.Uri import android.os.SystemClock @@ -363,7 +364,7 @@ class AlarmNotificationManager @Inject constructor( ) { val builder = NotificationCompat.Builder(context, CHANNEL_FULL_SCREEN_SILENT) .setSmallIcon(iconsProvider.getNotificationIcon()) - .setLargeIcon(rh.decodeResource(iconsProvider.getIcon())) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, iconsProvider.getIcon())) .setContentTitle(title) .setContentText(body) .setStyle(NotificationCompat.BigTextStyle().bigText(body)) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt index cc2238f10d33..0a72dfce3621 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmSoundPlayerImpl.kt @@ -118,7 +118,7 @@ class AlarmSoundPlayerImpl @Inject constructor( MediaPlayer() } mp.setAudioAttributes(audioAttrs) - val afd = rh.openRawResourceFd(soundRes) ?: run { + val afd = context.resources.openRawResourceFd(soundRes) ?: run { aapsLogger.error(LTag.CORE, "AlarmSoundPlayer: unable to open raw resource $soundRes") mp.release() return diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt index ec309ec62613..700a140308d3 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt @@ -6,6 +6,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import androidx.core.app.NotificationCompat import androidx.core.app.TaskStackBuilder import app.aaps.core.interfaces.notifications.NotificationHolder @@ -47,7 +48,7 @@ class NotificationHolderImpl @Inject constructor( .setOnlyAlertOnce(true) .setCategory(NotificationCompat.CATEGORY_STATUS) .setSmallIcon(iconsProvider.getNotificationIcon()) - .setLargeIcon(rh.decodeResource(iconsProvider.getIcon())) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, iconsProvider.getIcon())) .setContentTitle(rh.gs(app.aaps.core.ui.R.string.loading)) .setContentIntent(openAppIntent()) .build() diff --git a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt index d209cac1335c..82cb8ef37ffc 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt @@ -41,6 +41,7 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.minutes import android.app.NotificationManager as AndroidNotificationManager +import android.graphics.BitmapFactory @Singleton class NotificationManagerImpl @Inject constructor( @@ -340,7 +341,7 @@ class NotificationManagerImpl @Inject constructor( private fun raiseSystemNotification(n: AapsNotification) { val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as AndroidNotificationManager - val largeIcon = rh.decodeResource(iconsProvider.getIcon()) + val largeIcon = BitmapFactory.decodeResource(context.resources, iconsProvider.getIcon()) val smallIcon = iconsProvider.getNotificationIcon() val sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) val notificationBuilder = NotificationCompat.Builder(context, NotificationManager.CHANNEL_ID) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt index 329465b89a4d..e7c6639dd451 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt @@ -1,14 +1,9 @@ package app.aaps.implementation.resources import android.content.Context -import android.content.res.AssetFileDescriptor import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.annotation.ColorRes import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import androidx.core.content.ContextCompat import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey @@ -82,14 +77,6 @@ class ResourceHelperImpl @Inject constructor(var context: Context, private val f context.createConfigurationContext(this).getString(id, *args) } - override fun gc(@ColorRes id: Int): Int = ContextCompat.getColor(context, id) - - override fun openRawResourceFd(id: Int): AssetFileDescriptor = - context.resources.openRawResourceFd(id) - - override fun decodeResource(id: Int): Bitmap = - BitmapFactory.decodeResource(context.resources, id) - // Reads the bool resource directly. It used to go through gb(), which was dropped from the // interface because this was its only caller. override fun shortTextMode(): Boolean = !context.resources.getBoolean(app.aaps.core.ui.R.bool.isTablet) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index 27592110cf7b..ddda0054538b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -4,6 +4,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput import app.aaps.core.data.model.GlucoseUnit @@ -253,7 +254,7 @@ class PersistentNotificationPlugin @Inject constructor( if (includeAuto && unreadConversationBuilder != null) { builder.extend( NotificationCompat.CarExtender() - .setLargeIcon(rh.decodeResource(iconsProvider.getIcon())) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, iconsProvider.getIcon())) .setUnreadConversation(unreadConversationBuilder.build()) ) } diff --git a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt index ce58f4cd2699..a32c9c13005a 100644 --- a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt +++ b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplTest.kt @@ -1,8 +1,6 @@ package app.aaps.shared.impl.utils import android.content.Context -import android.content.res.AssetFileDescriptor -import android.graphics.Bitmap import android.text.format.DateFormat import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat @@ -1526,9 +1524,6 @@ class DateUtilImplTest { // --- Dummy implementations for the rest of the interface --- override fun gq(id: Int, quantity: Int, vararg args: Any?): String = "" override fun gsNotLocalised(id: Int, vararg args: Any?): String = "" - override fun gc(id: Int): Int = 0 - override fun openRawResourceFd(id: Int): AssetFileDescriptor? = null - override fun decodeResource(id: Int): Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) override fun shortTextMode(): Boolean = true } } diff --git a/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphBitmapRenderer.kt b/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphBitmapRenderer.kt index 76c97a7df625..6575800c20fb 100644 --- a/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphBitmapRenderer.kt +++ b/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphBitmapRenderer.kt @@ -23,7 +23,26 @@ data class BgGraphColors( val low: Int, val inRange: Int, val high: Int -) +) { + + companion object { + + /** + * The colors a widget draws BG in. They are the same values as the `widget_low`, + * `widget_inrange` and `widget_high` resources, which have no `values-night` variant, so + * reading them through resources could only ever return these constants. + * + * They are deliberately not the [app.aaps.core.ui.compose.AapsTheme] BG colors: a widget sits + * on the launcher over unknown wallpaper, not inside the app, so it keeps one high-contrast + * set instead of following the app's light and dark themes. + */ + val WIDGET = BgGraphColors( + low = 0xFFFF0000.toInt(), + inRange = 0xFF00FF00.toInt(), + high = 0xFFFFFF00.toInt() + ) + } +} /** * Minimal Canvas BG-graph renderer producing a transparent bitmap. diff --git a/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphStateLoader.kt b/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphStateLoader.kt index eb59bca49659..17318e5203eb 100644 --- a/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphStateLoader.kt +++ b/ui/src/main/kotlin/app/aaps/ui/widget/glance/BgGraphStateLoader.kt @@ -78,11 +78,7 @@ class BgGraphStateLoader @Inject constructor( yMaxUserUnits = yMax ) - val colors = BgGraphColors( - low = rh.gc(app.aaps.core.ui.R.color.widget_low), - inRange = rh.gc(app.aaps.core.ui.R.color.widget_inrange), - high = rh.gc(app.aaps.core.ui.R.color.widget_high) - ) + val colors = BgGraphColors.WIDGET // Current BG + trend (same logic as WidgetStateLoader) val lastBg = lastBgData.lastBg() diff --git a/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt b/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt index 27c4009c9b73..b3f305dd1788 100644 --- a/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt +++ b/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt @@ -63,9 +63,9 @@ class WidgetStateLoader @Inject constructor( val bgText = lastBg?.let { profileUtil.fromMgdlToStringInUnits(it.recalculated) } ?: rh.gs(app.aaps.core.ui.R.string.value_unavailable_short) val bgColor = when { - lastBgData.isLow() -> rh.gc(app.aaps.core.ui.R.color.widget_low) - lastBgData.isHigh() -> rh.gc(app.aaps.core.ui.R.color.widget_high) - else -> rh.gc(app.aaps.core.ui.R.color.widget_inrange) + lastBgData.isLow() -> BgGraphColors.WIDGET.low + lastBgData.isHigh() -> BgGraphColors.WIDGET.high + else -> BgGraphColors.WIDGET.inRange } val strikeThrough = !lastBgData.isActualBg() From a9c537a26524109b74c2456af3bc0571b5d9bdf4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 22:02:09 +0200 Subject: [PATCH 101/146] Add TextResolver, the part of ResourceHelper shared code can use Most files in :core:interfaces never call a resolver. They only name the type in a signature, for example `fun minAgo(rh: ResourceHelper, time: Long?)`. Moving all of androidMain to commonMain and compiling for iosArm64 gives 24 errors that name ResourceHelper, and almost all of them are signatures like that: DateUtil mentions it 15 times and calls it zero times, Profile 6 and zero. Only 8 real calls exist in the whole module, in PluginBase, PumpSync, PumpPluginBase and InsulinType. So the type is the blocker, not the call sites. TextResolver is the part every platform can implement - resolving a TextRef - and ResourceHelper now extends it, keeping the resource id overloads that only mean something on Android. A file that only names the type can switch to TextResolver and move to commonMain, with no change at its callers, because every ResourceHelper is a TextResolver. Verified by moving DateUtil across: with the one word changed it compiles for iosArm64. That move is not included here, because DateUtilImpl then has to resolve through TextRef instead of R.string ids, which is its own piece of work per file. An expect interface with an actual typealias was tried first and does not work here: Dagger cannot resolve a Kotlin typealias, and the Omnipod Eros driver has Java files that import ResourceHelper directly, which stops being a class once it is aliased. A plain supertype avoids both problems and needs no iosMain yet. --- .../interfaces/resources/ResourceHelper.kt | 10 +++--- .../core/interfaces/resources/TextResolver.kt | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/resources/TextResolver.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index fd285dfacccd..f62c14b7a282 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -7,7 +7,7 @@ import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs -interface ResourceHelper { +interface ResourceHelper : TextResolver { fun gs(@StringRes id: Int): String fun gs(@StringRes id: Int, vararg args: Any?): String @@ -24,7 +24,7 @@ interface ResourceHelper { * [TextRef.Named] still ends up in `Resources` on Android - the name is turned into an id first - * so locale matching behaves exactly as it does for [TextRef.AndroidRes]. */ - fun gs(ref: TextRef): String = when (ref) { + override fun gs(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text is TextRef.AndroidRes -> if (ref.args.isEmpty()) gs(ref.id) @@ -41,10 +41,10 @@ interface ResourceHelper { } /** Same, with format arguments - mirrors `gs(id, vararg)`. */ - fun gs(ref: TextRef, vararg args: Any): String = gs(ref.withArgs(*args)) + override fun gs(ref: TextRef, vararg args: Any): String = gs(ref.withArgs(*args)) /** Same, but always in English - used to build the search index. */ - fun gsNotLocalised(ref: TextRef): String = when (ref) { + override fun gsNotLocalised(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text is TextRef.AndroidRes -> gsNotLocalised(ref.id, *ref.args.toTypedArray()) is TextRef.Named -> keysIdOf(ref) @@ -53,7 +53,7 @@ interface ResourceHelper { } - fun shortTextMode(): Boolean + override fun shortTextMode(): Boolean } /** diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/resources/TextResolver.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/resources/TextResolver.kt new file mode 100644 index 000000000000..900414cd0ade --- /dev/null +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/resources/TextResolver.kt @@ -0,0 +1,31 @@ +package app.aaps.core.interfaces.resources + +import app.aaps.core.keys.interfaces.TextRef + +/** + * Turns a [TextRef] into a String outside Compose. Inside Compose use + * `app.aaps.core.ui.compose.stringResource` instead. + * + * This is the part of string resolution that every platform can implement, so it is what shared code + * depends on. On Android `ResourceHelper` extends it and adds the resource id overloads, which only + * mean something where Android resources exist. + * + * The split exists because most files in this module never call a resolver - they only name the type + * in a signature, for example `fun minAgo(rh: ResourceHelper, time: Long?)`. Naming [TextResolver] + * instead lets those files move to commonMain without any change to their callers, since every + * `ResourceHelper` is a [TextResolver]. + */ +interface TextResolver { + + /** Resolves [ref] in the current language. */ + fun gs(ref: TextRef): String + + /** Same, with format arguments. */ + fun gs(ref: TextRef, vararg args: Any): String + + /** Same, but always in English - used for data that is stored or uploaded, not displayed. */ + fun gsNotLocalised(ref: TextRef): String + + /** True on a small screen, where the UI uses shorter texts. */ + fun shortTextMode(): Boolean +} From 949a587cd5263e55351c5d128c39e4bda816048f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 22:11:22 +0200 Subject: [PATCH 102/146] Move to commonMain everything that already compiles there 22 files in :core:interfaces needed no change at all to build for iosArm64, so they move. They are the leaf value types and small contracts: PureProfile, SingleProfile, ProfileStore, ProfileRepository, ProfileValidationError, APSResult, RT, PumpInsulin, PumpRate, BolusProgressData, DetailedBolusInfoStorage, MappedStateFlow, ConcentrationHelper, DecimalFormatter, CustomCommand, PumpPluginConstraints, Automation, ProcessedDeviceStatusData, PrefMetadata, AlarmSoundPlayer, PermissionGroup and PermissionProvider. Found by moving all of androidMain to commonMain, compiling for iosArm64, moving the failures back, and repeating until the build is green. That took six rounds: 32 files fail on their own Android imports, then 46 more because they referenced those 32, then 19, then 10, then 1. One round is not enough and gives a much larger answer, because a file can compile only while a dependency that later has to move back is still next to it. 108 files stay in androidMain. The point is not the 22 files, it is that the iosArm64 target now fails the build if an Android import appears in any of them. --- .../kotlin/app/aaps/core/interfaces/aps/APSResult.kt | 0 .../kotlin/app/aaps/core/interfaces/aps/RT.kt | 0 .../kotlin/app/aaps/core/interfaces/automation/Automation.kt | 0 .../app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt | 0 .../app/aaps/core/interfaces/insulin/ConcentrationHelper.kt | 0 .../kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt | 0 .../app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt | 0 .../aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt | 0 .../kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt | 0 .../kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt | 0 .../kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt | 0 .../kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt | 0 .../app/aaps/core/interfaces/profile/ProfileValidationError.kt | 0 .../kotlin/app/aaps/core/interfaces/profile/PureProfile.kt | 0 .../kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt | 0 .../app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/PumpRate.kt | 0 .../kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt | 0 .../kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt | 0 22 files changed, 0 insertions(+), 0 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/APSResult.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/RT.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/automation/Automation.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt (100%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/APSResult.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/RT.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/RT.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/RT.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/RT.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/automation/Automation.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/PumpPluginConstraints.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/ConcentrationHelper.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/maintenance/PrefMetadata.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/AlarmSoundPlayer.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/nsclient/ProcessedDeviceStatusData.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PermissionGroup.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PermissionProvider.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileRepository.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileStore.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileValidationError.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/PureProfile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/SingleProfile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/BolusProgressData.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/DetailedBolusInfoStorage.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/MappedStateFlow.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpInsulin.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpRate.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/CustomCommand.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/DecimalFormatter.kt From fbcbfa297491bf16727d312dd00b3c17ac9b6370 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 22:35:38 +0200 Subject: [PATCH 103/146] DateUtil resolves through TextResolver, and moves to commonMain DateUtil named ResourceHelper in 14 signatures and never called it, which is what kept it and everything downstream on Android. The signatures now take TextResolver. Callers are untouched: they pass a ResourceHelper, which is a TextResolver. DateUtilImpl keeps resolving through Android resources, it just says so explicitly - rh.gs(R.string.x) becomes rh.gs(TextRef.AndroidRes(R.string.x)), 24 call sites. One method could not come along. timeAgoFullString uses plurals, and a TextRef cannot carry a plural today, so it leaves the interface and becomes an Android extension next to the plurals it needs. It had exactly one caller, in the Medtrum overview, which is Android only anyway. Re-running the fixpoint puts DateUtil, TrendCalculator, AutosensDataStore and ClockSkewCompensation in commonMain: 154 files there now, 105 left. AutosensDataStore is the interesting one - androidx.collection.LongSparseArray was never the problem, it is multiplatform and already on the commonMain classpath. It was waiting for DateUtil the whole time, and Sensitivity and IobCobCalculator are waiting on it. DateUtilImplOldTest stubbed rh.gs with a raw id and now stubs the TextRef. --- .../core/interfaces/utils/DateUtilAndroid.kt | 31 ++++++ .../core/interfaces/aps/AutosensDataStore.kt | 0 .../interfaces/db/ClockSkewCompensation.kt | 0 .../aaps/core/interfaces/utils/DateUtil.kt | 29 +++--- .../core/interfaces/utils/TrendCalculator.kt | 0 .../compose/MedtrumOverviewViewModel.kt | 3 +- .../aaps/shared/impl/utils/DateUtilImpl.kt | 94 ++++++++----------- .../shared/impl/utils/DateUtilImplOldTest.kt | 3 +- 8 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtilAndroid.kt rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt (95%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt (100%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtilAndroid.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtilAndroid.kt new file mode 100644 index 000000000000..6a85c0875b0e --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtilAndroid.kt @@ -0,0 +1,31 @@ +package app.aaps.core.interfaces.utils + +import app.aaps.core.interfaces.R +import app.aaps.core.interfaces.resources.ResourceHelper +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds + +/** + * Formats an age as "2 days 3 hours ago". + * + * This is an Android extension rather than a [DateUtil] member because it needs plurals, and a + * [app.aaps.core.keys.interfaces.TextRef] cannot carry a plural today. Everything else on [DateUtil] + * resolves through [app.aaps.core.interfaces.resources.TextResolver] and works on any platform. + */ +fun timeAgoFullString(milliseconds: Long, rh: ResourceHelper): String = + when { + milliseconds <= 0 -> "" + else -> { + val duration = milliseconds.milliseconds + val days = duration.inWholeDays + val hours = (duration - days.days).inWholeHours + val minutes = (duration - days.days - hours.hours).inWholeMinutes + when { + days > 0 -> rh.gq(R.plurals.plurals_day_hour_ago, days.toInt(), days.toString(), hours.toString()) + hours > 0 -> rh.gq(R.plurals.plurals_hour_ago, hours.toInt(), hours.toString()) + minutes > 0 -> rh.gq(R.plurals.plurals_minute_ago, minutes.toInt(), minutes.toString()) + else -> rh.gs(R.string.seconds_ago) + } + } + } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/AutosensDataStore.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/ClockSkewCompensation.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt similarity index 95% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt index ae665901fd1a..64a8e85a3c45 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/DateUtil.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.utils -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver /** * The Class DateUtil. A modern utility class for handling dates, times, and durations using the `java.time` API. @@ -73,7 +73,7 @@ interface DateUtil { * @param rh A resource helper to get localized strings like "Today". * @return The relative date string. */ - fun dateStringRelative(mills: Long, rh: ResourceHelper): String + fun dateStringRelative(mills: Long, rh: TextResolver): String /** * Formats a timestamp into a short date string (e.g., "10/27" or "27/10") based on the user's 12/24 hour preference. @@ -254,7 +254,7 @@ interface DateUtil { * @param time The timestamp in milliseconds. Can be null. * @return The relative time string. */ - fun minAgo(rh: ResourceHelper, time: Long?): String + fun minAgo(rh: TextResolver, time: Long?): String /** * Returns a string describing how many minutes or seconds ago a timestamp was. @@ -263,7 +263,7 @@ interface DateUtil { * @param time The timestamp in milliseconds. Can be null. * @return The relative time string (e.g., "30 sec ago" or "3 min ago"). */ - fun minOrSecAgo(rh: ResourceHelper, time: Long?): String + fun minOrSecAgo(rh: TextResolver, time: Long?): String /** * Formats a duration into a short bracketed string with a leading '+', using seconds under @@ -272,7 +272,7 @@ interface DateUtil { * @param durationMs The duration in milliseconds. Negative values return an empty string. * @return The formatted duration string, or empty if negative. */ - fun minOrSec(rh: ResourceHelper, durationMs: Long): String + fun minOrSec(rh: TextResolver, durationMs: Long): String /** * Returns a short string showing the difference in minutes between now and a given time, with a sign. @@ -288,7 +288,7 @@ interface DateUtil { * @param time The timestamp in milliseconds. Can be null. * @return The verbose relative time string. */ - fun minAgoLong(rh: ResourceHelper, time: Long?): String + fun minAgoLong(rh: TextResolver, time: Long?): String /** * Returns a string describing how many hours ago a timestamp was. @@ -296,7 +296,7 @@ interface DateUtil { * @param rh Resource helper for localized strings. * @return The relative hours string. */ - fun hourAgo(time: Long, rh: ResourceHelper): String + fun hourAgo(time: Long, rh: TextResolver): String /** * Returns a string describing how many days ago (or in how many days) a timestamp is. @@ -305,7 +305,7 @@ interface DateUtil { * @param round If true, rounds to the nearest whole day. Otherwise uses fractional days. * @return The relative days string. */ - fun dayAgo(time: Long, rh: ResourceHelper, round: Boolean = false): String + fun dayAgo(time: Long, rh: TextResolver, round: Boolean = false): String /** * Calculates the timestamp for the beginning of the day (midnight) for a given timestamp. @@ -328,7 +328,7 @@ interface DateUtil { * @param withParentheses Whether to wrap the result in parentheses (e.g. "(30')" vs "30'"). Defaults to true. * @return A formatted duration string like "(1h 30')". */ - fun timeFrameString(timeInMillis: Long, rh: ResourceHelper, withParentheses: Boolean = true): String + fun timeFrameString(timeInMillis: Long, rh: TextResolver, withParentheses: Boolean = true): String /** * Calculates the elapsed time since a given timestamp and formats it as a duration. @@ -336,7 +336,7 @@ interface DateUtil { * @param rh Resource helper. * @return A formatted duration string of the elapsed time. */ - fun sinceString(timestamp: Long, rh: ResourceHelper): String + fun sinceString(timestamp: Long, rh: TextResolver): String /** * Calculates the time remaining until a future timestamp and formats it as a duration. @@ -345,7 +345,7 @@ interface DateUtil { * @param withParentheses Whether to wrap the result in parentheses (e.g. "(30')" vs "30'"). Defaults to true. * @return A formatted duration string of the remaining time. */ - fun untilString(timestamp: Long, rh: ResourceHelper, withParentheses: Boolean = true): String + fun untilString(timestamp: Long, rh: TextResolver, withParentheses: Boolean = true): String /** * Formats a remaining duration as a localized "time remaining" string. @@ -356,7 +356,7 @@ interface DateUtil { * @param rh Resource helper for the localized format string. * @return The formatted "time remaining" string. */ - fun timeRemainingString(timeInMillis: Long, rh: ResourceHelper): String + fun timeRemainingString(timeInMillis: Long, rh: TextResolver): String /** * Gets the current system time in milliseconds. @@ -438,8 +438,7 @@ interface DateUtil { * @param rh Resource helper to get localized unit strings. * @return The formatted age string. */ - fun age(milliseconds: Long, useShortText: Boolean, rh: ResourceHelper): String - fun timeAgoFullString(milliseconds: Long, rh: ResourceHelper): String + fun age(milliseconds: Long, useShortText: Boolean, rh: TextResolver): String /** @@ -449,7 +448,7 @@ interface DateUtil { * @param rh Resource helper to get localized unit strings (e.g., "second", "seconds"). * @return The formatted string with a single unit (e.g., "5 days"). */ - fun niceTimeScalar(time: Long, rh: ResourceHelper): String + fun niceTimeScalar(time: Long, rh: TextResolver): String /** * A thread-safe, locale-agnostic utility to format a double into a string with a specific number of decimal digits. * It is optimized to use a cached formatter on the UI thread. diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/utils/TrendCalculator.kt diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt index b98d6df2d64b..41ed790395d1 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModel.kt @@ -23,6 +23,7 @@ import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.timeAgoFullString import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.StatusLevel import app.aaps.core.ui.compose.pump.ActionCategory @@ -279,7 +280,7 @@ class MedtrumOverviewViewModel @Inject constructor( // Patch age if (medtrumPump.patchStartTime != 0L) { val age = System.currentTimeMillis() - medtrumPump.patchStartTime - val agoString = dateUtil.timeAgoFullString(age, rh) + val agoString = timeAgoFullString(age, rh) val ageString = dateUtil.dateAndTimeString(medtrumPump.patchStartTime) + "\n" + agoString add(PumpInfoRow(label = rh.gs(R.string.patch_activation_time_label), value = ageString)) } diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt index 4b49427fc603..752bdf7b5992 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/utils/DateUtilImpl.kt @@ -5,7 +5,8 @@ import androidx.collection.LongSparseArray import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.format.NumberFormatPlatform import app.aaps.core.interfaces.R -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.SafeParse import app.aaps.core.interfaces.utils.TimeDiff @@ -110,20 +111,20 @@ class DateUtilImpl @Inject constructor( override fun dateString(mills: Long): String = Instant.ofEpochMilli(mills).atZone(systemZone).format(getLocalizedDateFormatter()) - override fun dateStringRelative(mills: Long, rh: ResourceHelper): String { // Get the current time and the start of today as simple millisecond timestamps. + override fun dateStringRelative(mills: Long, rh: TextResolver): String { // Get the current time and the start of today as simple millisecond timestamps. val nowMillis = now() val startOfTodayMillis = beginOfDay(nowMillis) return if (mills < nowMillis) { // Past when { - mills > startOfTodayMillis -> "${rh.gs(R.string.today)} - ${dateString(mills)}" - mills > startOfTodayMillis - 1.days.inWholeMilliseconds -> "${rh.gs(R.string.yesterday)} - ${dateString(mills)}" + mills > startOfTodayMillis -> "${rh.gs(TextRef.AndroidRes(R.string.today))} - ${dateString(mills)}" + mills > startOfTodayMillis - 1.days.inWholeMilliseconds -> "${rh.gs(TextRef.AndroidRes(R.string.yesterday))} - ${dateString(mills)}" mills > startOfTodayMillis - 7.days.inWholeMilliseconds -> "${dayAgo(mills, rh, true)} - ${dateString(mills)}" else -> dateString(mills) } } else { // Future when { - mills < startOfTodayMillis + 1.days.inWholeMilliseconds -> rh.gs(R.string.later_today) - mills < startOfTodayMillis + 2.days.inWholeMilliseconds -> rh.gs(R.string.tomorrow) + mills < startOfTodayMillis + 1.days.inWholeMilliseconds -> rh.gs(TextRef.AndroidRes(R.string.later_today)) + mills < startOfTodayMillis + 2.days.inWholeMilliseconds -> rh.gs(TextRef.AndroidRes(R.string.tomorrow)) mills < startOfTodayMillis + 7.days.inWholeMilliseconds -> dayAgo(mills, rh, true) else -> dateString(mills) } @@ -199,32 +200,32 @@ class DateUtilImpl @Inject constructor( override fun dateAndTimeAndSecondsString(mills: Long): String = if (mills == 0L) "" else dateString(mills) + " " + timeStringWithSeconds(mills) - override fun minAgo(rh: ResourceHelper, time: Long?): String { + override fun minAgo(rh: TextResolver, time: Long?): String { if (time == null) return "" val duration = (now() - time).milliseconds val minutes = duration.inWholeMinutes.toInt() - return if (abs(minutes) > 9999) "" else rh.gs(R.string.minago, minutes) + return if (abs(minutes) > 9999) "" else rh.gs(TextRef.AndroidRes(R.string.minago), minutes) } - override fun minOrSecAgo(rh: ResourceHelper, time: Long?): String { + override fun minOrSecAgo(rh: TextResolver, time: Long?): String { if (time == null) return "" val duration = (now() - time).milliseconds return when { duration.inWholeMinutes >= 2 -> { // If the duration is 2 minutes or more, show minutes - rh.gs(R.string.minago, duration.inWholeMinutes.toInt()) + rh.gs(TextRef.AndroidRes(R.string.minago), duration.inWholeMinutes.toInt()) } else -> { // Otherwise, show seconds - rh.gs(R.string.secago, duration.inWholeSeconds.toInt()) + rh.gs(TextRef.AndroidRes(R.string.secago), duration.inWholeSeconds.toInt()) } } } - override fun minOrSec(rh: ResourceHelper, durationMs: Long): String { + override fun minOrSec(rh: TextResolver, durationMs: Long): String { if (durationMs < 0) return "" val duration = durationMs.milliseconds return when { - duration.inWholeMinutes >= 2 -> rh.gs(R.string.min_plus, duration.inWholeMinutes.toInt()) - else -> rh.gs(R.string.sec_plus, duration.inWholeSeconds.toInt()) + duration.inWholeMinutes >= 2 -> rh.gs(TextRef.AndroidRes(R.string.min_plus), duration.inWholeMinutes.toInt()) + else -> rh.gs(TextRef.AndroidRes(R.string.sec_plus), duration.inWholeSeconds.toInt()) } } @@ -236,35 +237,35 @@ class DateUtilImpl @Inject constructor( else "(" + (if (minutes > 0) "+" else "") + minutes + ")" } - override fun minAgoLong(rh: ResourceHelper, time: Long?): String { + override fun minAgoLong(rh: TextResolver, time: Long?): String { if (time == null) return "" val duration = (now() - time).milliseconds val minutes = duration.inWholeMinutes.toInt() - return if (abs(minutes) > 9999) "" else rh.gs(R.string.minago_long, minutes) + return if (abs(minutes) > 9999) "" else rh.gs(TextRef.AndroidRes(R.string.minago_long), minutes) } - override fun hourAgo(time: Long, rh: ResourceHelper): String { + override fun hourAgo(time: Long, rh: TextResolver): String { val duration = (now() - time).milliseconds val hours = duration.inWholeHours - return rh.gs(R.string.hoursago, hours) + return rh.gs(TextRef.AndroidRes(R.string.hoursago), hours) } - override fun dayAgo(time: Long, rh: ResourceHelper, round: Boolean): String { + override fun dayAgo(time: Long, rh: TextResolver, round: Boolean): String { val duration = (now() - time).milliseconds if (round) { val daysAsDouble = duration.toDouble(DurationUnit.DAYS) return if (duration.isPositive()) { val roundedDays = ceil(daysAsDouble) - rh.gs(R.string.days_ago_round, roundedDays) + rh.gs(TextRef.AndroidRes(R.string.days_ago_round), roundedDays) } else { val roundedDays = floor(daysAsDouble) - rh.gs(R.string.in_days_round, roundedDays) + rh.gs(TextRef.AndroidRes(R.string.in_days_round), roundedDays) } } return if (duration.isPositive()) { - rh.gs(R.string.days_ago, duration.inWholeDays) + rh.gs(TextRef.AndroidRes(R.string.days_ago), duration.inWholeDays) } else { - rh.gs(R.string.in_days, abs(duration.inWholeDays)) + rh.gs(TextRef.AndroidRes(R.string.in_days), abs(duration.inWholeDays)) } } @@ -281,29 +282,29 @@ class DateUtilImpl @Inject constructor( return t } - override fun timeFrameString(timeInMillis: Long, rh: ResourceHelper, withParentheses: Boolean): String { + override fun timeFrameString(timeInMillis: Long, rh: TextResolver, withParentheses: Boolean): String { val duration = timeInMillis.milliseconds val totalHours = duration.inWholeHours val remainingMinutes = (duration - totalHours.hours).inWholeMinutes - val hoursPart = if (totalHours > 0) "$totalHours${rh.gs(R.string.shorthour)} " else "" + val hoursPart = if (totalHours > 0) "$totalHours${rh.gs(TextRef.AndroidRes(R.string.shorthour))} " else "" val body = "$hoursPart$remainingMinutes'" return if (withParentheses) "($body)" else body } - override fun sinceString(timestamp: Long, rh: ResourceHelper): String = + override fun sinceString(timestamp: Long, rh: TextResolver): String = timeFrameString(now() - timestamp, rh) - override fun untilString(timestamp: Long, rh: ResourceHelper, withParentheses: Boolean): String { + override fun untilString(timestamp: Long, rh: TextResolver, withParentheses: Boolean): String { val durationMillis = timestamp - now() return timeFrameString(durationMillis, rh, withParentheses) } - override fun timeRemainingString(timeInMillis: Long, rh: ResourceHelper): String { + override fun timeRemainingString(timeInMillis: Long, rh: TextResolver): String { val duration = timeInMillis.milliseconds val totalHours = duration.inWholeHours.toInt() val remainingMinutes = (duration - totalHours.hours).inWholeMinutes.toInt() - return if (totalHours > 0) rh.gs(R.string.time_remaining_h_m, totalHours, remainingMinutes) - else rh.gs(R.string.time_remaining_m, remainingMinutes) + return if (totalHours > 0) rh.gs(TextRef.AndroidRes(R.string.time_remaining_h_m), totalHours, remainingMinutes) + else rh.gs(TextRef.AndroidRes(R.string.time_remaining_m), remainingMinutes) } override fun now(): Long = clock.millis() @@ -364,31 +365,12 @@ class DateUtilImpl @Inject constructor( } } - override fun timeAgoFullString(milliseconds: Long, rh: ResourceHelper): String { - return when { - milliseconds <= 0 -> "" - else -> { - val duration = milliseconds.milliseconds - val days = duration.inWholeDays - val hours = (duration - days.days).inWholeHours - val minutes = (duration - days.days - hours.hours).inWholeMinutes - - when { - days > 0 -> rh.gq(R.plurals.plurals_day_hour_ago, days.toInt(), days.toString(), hours.toString()) - hours > 0 -> rh.gq(R.plurals.plurals_hour_ago, hours.toInt(), hours.toString()) - minutes > 0 -> rh.gq(R.plurals.plurals_minute_ago, minutes.toInt(), minutes.toString()) - else -> rh.gs(R.string.seconds_ago) - } - } - } - } - - override fun age(milliseconds: Long, useShortText: Boolean, rh: ResourceHelper): String { + override fun age(milliseconds: Long, useShortText: Boolean, rh: TextResolver): String { val duration = milliseconds.milliseconds - if (duration.inWholeDays > 1000) return rh.gs(R.string.forever) - val daysUnit = if (useShortText) rh.gs(R.string.shortday) else rh.gs(R.string.days) - val hoursUnit = if (useShortText) rh.gs(R.string.shorthour) else rh.gs(R.string.hours) - val minutesUnit = if (useShortText) rh.gs(R.string.shortminute) else rh.gs(R.string.unit_minutes) + if (duration.inWholeDays > 1000) return rh.gs(TextRef.AndroidRes(R.string.forever)) + val daysUnit = if (useShortText) rh.gs(TextRef.AndroidRes(R.string.shortday)) else rh.gs(TextRef.AndroidRes(R.string.days)) + val hoursUnit = if (useShortText) rh.gs(TextRef.AndroidRes(R.string.shorthour)) else rh.gs(TextRef.AndroidRes(R.string.hours)) + val minutesUnit = if (useShortText) rh.gs(TextRef.AndroidRes(R.string.shortminute)) else rh.gs(TextRef.AndroidRes(R.string.unit_minutes)) val days = duration.inWholeDays val hours = (duration - days.days).inWholeHours val minutes = (duration - days.days - hours.hours).inWholeMinutes @@ -399,7 +381,7 @@ class DateUtilImpl @Inject constructor( } } - override fun niceTimeScalar(time: Long, rh: ResourceHelper): String { + override fun niceTimeScalar(time: Long, rh: TextResolver): String { val duration = time.milliseconds val (value, unitId) = when { duration.inWholeDays > 6 -> { @@ -423,7 +405,7 @@ class DateUtilImpl @Inject constructor( seconds to if (seconds == 1L) R.string.unit_second else R.string.unit_seconds } } - return "${qs(value.toDouble(), 0)} ${rh.gs(unitId)}" + return "${qs(value.toDouble(), 0)} ${rh.gs(TextRef.AndroidRes(unitId))}" } override fun qs(x: Double, numDigits: Int): String { diff --git a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplOldTest.kt b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplOldTest.kt index 2578219e86fd..9beb1c6e1766 100644 --- a/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplOldTest.kt +++ b/shared/impl/src/test/kotlin/app/aaps/shared/impl/utils/DateUtilImplOldTest.kt @@ -2,6 +2,7 @@ package app.aaps.shared.impl.utils import android.content.Context import app.aaps.core.data.time.T +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.R import app.aaps.core.interfaces.resources.ResourceHelper import com.google.common.truth.Truth @@ -90,7 +91,7 @@ class DateUtilImplOldTest() { } */ @Test fun timeFrameStringTest() { - whenever(rh.gs(R.string.shorthour)).thenReturn("h") + whenever(rh.gs(TextRef.AndroidRes(R.string.shorthour))).thenReturn("h") Truth.assertThat(DateUtilImpl(context).timeFrameString(T.Companion.hours(1).msecs() + T.Companion.mins(1).msecs(), rh)).isEqualTo("(1h 1')") } } \ No newline at end of file From 2b88e8e00928dbcbd16bd8ed88b1b4ac13a277b2 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 15 Aug 2026 23:04:53 +0200 Subject: [PATCH 104/146] NotificationManager takes a TextRef, and moves to commonMain NotificationManager is our own interface, not the Android one, and @StringRes was its only platform import. The resource id overload of post() becomes a TextRef overload, so the interface no longer names Android resources and moves to commonMain next to AapsNotification, which was already there. The 68 call sites pass TextRef.AndroidRes(R.string.x) now. An Android extension keeping the old id form was tried first and dropped: it would have needed an import in the same 51 files and left the id habit in place, so the churn would have been paid twice. Three call sites resolved eagerly with rh.gs and now pass the ref instead, so the text is resolved when the notification is shown rather than when it is posted. FAILED_UPDATE_PROFILE keeps the String overload on purpose - it carries result.comment, a runtime string. Two arguments were nullable and the old vararg accepted null. They are now .toString(), which keeps what String.format did with a null. MedtronicUtil.sendNotification passes an id from an enum plus a vararg spread, so it wraps the whole thing once instead of forwarding. --- .../TestOpenAPSSMBDynamicISFPlugin.kt | 6 +- app/src/main/kotlin/app/aaps/MainApp.kt | 6 +- .../notifications/NotificationManager.kt | 12 +++- .../core/objects/profile/ProfileSealed.kt | 7 ++- .../alerts/LocalAlertUtilsImpl.kt | 5 +- .../notifications/NotificationManagerImpl.kt | 6 +- .../profile/ProfileRepositoryImpl.kt | 4 +- .../pump/PumpSyncImplementation.kt | 3 +- .../queue/CommandQueueImplementation.kt | 7 ++- .../queue/CommandQueueImplementationTest.kt | 14 +++-- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 7 ++- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 12 ++-- .../configBuilder/RunningConfigurationImpl.kt | 3 +- .../constraints/dstHelper/DstHelperPlugin.kt | 10 ++-- .../constraints/safety/SafetyPlugin.kt | 2 +- .../SignatureVerifierPlugin.kt | 3 +- .../storage/StorageConstraintPlugin.kt | 3 +- .../versionChecker/VersionCheckerUtilsImpl.kt | 7 ++- .../app/aaps/plugins/source/AidexPlugin.kt | 11 ++-- .../clientcontrol/OrphanDetector.kt | 3 +- .../smsCommunicator/SmsCommunicatorPlugin.kt | 4 +- .../clientcontrol/OrphanDetectorTest.kt | 9 +-- .../nightscout/pump/combov2/ComboV2Plugin.kt | 29 ++++----- .../pump/danar/comm/MsgInitConnStatusBolus.kt | 3 +- .../danar/comm/MsgInitConnStatusOption.kt | 3 +- .../pump/danar/comm/MsgInitConnStatusTime.kt | 3 +- .../aaps/pump/danar/comm/MsgSettingMeal.kt | 5 +- .../services/AbstractDanaRExecutionService.kt | 13 ++-- .../danar/services/DanaRExecutionService.kt | 2 +- .../comm/MsgInitConnStatusBasicK.kt | 5 +- .../comm/MsgInitConnStatusBolusK.kt | 3 +- .../comm/MsgInitConnStatusTimeK.kt | 3 +- .../services/DanaRKoreanExecutionService.kt | 2 +- .../aaps/pump/danarv2/comm/MsgCheckValueV2.kt | 5 +- .../services/DanaRv2ExecutionService.kt | 2 +- .../comm/DanaRSPacketBasalGetBasalRate.kt | 3 +- .../comm/DanaRSPacketBolusGetBolusOption.kt | 3 +- .../comm/DanaRSPacketGeneralGetPumpCheck.kt | 3 +- ...RSPacketGeneralInitialScreenInformation.kt | 3 +- .../app/aaps/pump/danars/services/BLEComm.kt | 9 +-- .../pump/danars/services/DanaRSService.kt | 2 +- .../pump/diaconn/service/DiaconnG8Service.kt | 2 +- .../app/aaps/pump/insight/InsightPlugin.kt | 5 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 3 +- .../medtronic/data/MedtronicHistoryData.kt | 7 ++- .../aaps/pump/medtronic/util/MedtronicUtil.kt | 7 ++- .../pump/medtrum/services/MedtrumService.kt | 60 +++++++------------ .../omnipod/dash/OmnipodDashPumpPlugin.kt | 8 +-- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 26 ++++---- 49 files changed, 194 insertions(+), 169 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt (83%) diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt index e2c19c4983d8..80abb3bcd54d 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.aps.openAPSSMBDynamicISF import android.content.Context +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.time.T import app.aaps.core.interfaces.aps.DetermineBasalAdapter import app.aaps.core.interfaces.bgQualityCheck.BgQualityCheck @@ -96,10 +97,9 @@ class TestOpenAPSSMBDynamicISFPlugin @Inject constructor( if (tdd1D == null || tdd7D == null || tddLast4H == null || tddLast8to4H == null || tddLast24H == null || !dynIsfEnabled.value()) { notificationManager.post( NotificationId.SMB_FALLBACK, - R.string.fallback_smb_no_tdd, + TextRef.AndroidRes(R.string.fallback_smb_no_tdd), level = NotificationLevel.INFO, - validTo = dateUtil.now() + T.mins(1).msecs() - ) + validTo = dateUtil.now() + T.mins(1).msecs()) DetermineBasalAdapterSMBJS(ScriptReader(), injector) } else { notificationManager.dismiss(NotificationId.SMB_FALLBACK) diff --git a/app/src/main/kotlin/app/aaps/MainApp.kt b/app/src/main/kotlin/app/aaps/MainApp.kt index 8923139922b9..48d9fdf41793 100644 --- a/app/src/main/kotlin/app/aaps/MainApp.kt +++ b/app/src/main/kotlin/app/aaps/MainApp.kt @@ -471,7 +471,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { if (config.isDev() && preferences.get(StringKey.MaintenanceIdentification).isBlank()) notificationManager.post( id = NotificationId.IDENTIFICATION_NOT_SET, - R.string.identification_not_set, + TextRef.AndroidRes(R.string.identification_not_set), level = NotificationLevel.INFO, actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.set)) {}), validityCheck = { config.isDev() && preferences.get(StringKey.MaintenanceIdentification).isBlank() } @@ -480,7 +480,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { if (preferences.get(StringKey.ProtectionMasterPassword) == "") notificationManager.post( id = NotificationId.MASTER_PASSWORD_NOT_SET, - app.aaps.core.ui.R.string.master_password_not_set, + TextRef.AndroidRes(app.aaps.core.ui.R.string.master_password_not_set), level = NotificationLevel.NORMAL, actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.set)) {}), validityCheck = { preferences.get(StringKey.ProtectionMasterPassword) == "" } @@ -489,7 +489,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { if (preferences.getIfExists(StringKey.AapsDirectoryUri).isNullOrEmpty()) notificationManager.post( id = NotificationId.AAPS_DIR_NOT_SELECTED, - app.aaps.core.ui.R.string.aaps_directory_not_selected, + TextRef.AndroidRes(app.aaps.core.ui.R.string.aaps_directory_not_selected), level = NotificationLevel.LOW, actions = listOf(NotificationAction(TextRef.AndroidRes(R.string.select)) {}), validityCheck = { preferences.getIfExists(StringKey.AapsDirectoryUri).isNullOrEmpty() } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt similarity index 83% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt index 1c2da5d205c5..0c11c0d4c0c5 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/notifications/NotificationManager.kt @@ -1,6 +1,6 @@ package app.aaps.core.interfaces.notifications -import androidx.annotation.StringRes +import app.aaps.core.keys.interfaces.TextRef import kotlinx.coroutines.flow.StateFlow import kotlin.time.Clock @@ -32,10 +32,16 @@ interface NotificationManager { validityCheck: (() -> Boolean)? = null ): NotificationHandle + /** + * Posts a notification whose text is resolved when it is shown. + * + * Android callers keep writing `post(id, R.string.x, arg)`: that goes through the extension in + * `NotificationManagerAndroid`, which wraps the id in a [TextRef.AndroidRes]. Keeping the id out of + * this declaration is what lets the interface be platform neutral. + */ fun post( id: NotificationId, - @StringRes textRes: Int, - vararg formatArgs: Any?, + textRef: TextRef, level: NotificationLevel = id.defaultLevel, validMinutes: Int = 0, date: Long = Clock.System.now().toEpochMilliseconds(), diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index b39beae06162..98e25fc07741 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -1,5 +1,6 @@ package app.aaps.core.objects.profile +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.time.systemUtcOffsetAt import app.aaps.core.data.format.NumberFormat @@ -260,7 +261,7 @@ sealed class ProfileSealed( val duration: Long = basal.duration if (duration % 3600000 != 0L) { if (sendNotifications && config.APS) { - notificationManager.post(NotificationId.BASAL_PROFILE_NOT_ALIGNED_TO_HOURS, R.string.basalprofilenotaligned, from) + notificationManager.post(NotificationId.BASAL_PROFILE_NOT_ALIGNED_TO_HOURS, TextRef.AndroidRes(R.string.basalprofilenotaligned, listOf(from))) } validityCheck.isValid = false validityCheck.reasons.add( @@ -286,11 +287,11 @@ sealed class ProfileSealed( } protected open fun sendBelowMinimumNotification(from: String, notificationManager: NotificationManager, rh: ResourceHelper) { - notificationManager.post(NotificationId.MINIMAL_BASAL_VALUE_REPLACED, R.string.minimalbasalvaluereplaced, from) + notificationManager.post(NotificationId.MINIMAL_BASAL_VALUE_REPLACED, TextRef.AndroidRes(R.string.minimalbasalvaluereplaced, listOf(from))) } protected open fun sendAboveMaximumNotification(from: String, notificationManager: NotificationManager, rh: ResourceHelper) { - notificationManager.post(NotificationId.MAXIMUM_BASAL_VALUE_REPLACED, R.string.maximumbasalvaluereplaced, from) + notificationManager.post(NotificationId.MAXIMUM_BASAL_VALUE_REPLACED, TextRef.AndroidRes(R.string.maximumbasalvaluereplaced, listOf(from))) } override val units: GlucoseUnit diff --git a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt index 78f97190e07c..cdfa205a2f8d 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/alerts/LocalAlertUtilsImpl.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.alerts +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.TE import app.aaps.core.data.time.T import app.aaps.core.data.ue.Action @@ -68,7 +69,7 @@ class LocalAlertUtilsImpl @Inject constructor( if (preferences.get(BooleanKey.AlertPumpUnreachable)) { aapsLogger.debug(LTag.CORE, "Generating pump unreachable alarm. lastConnection: " + dateUtil.dateAndTimeString(lastConnection) + " isStatusOutdated: true") preferences.put(LocalAlertLongKey.NextPumpDisconnectedAlarm, dateUtil.now() + pumpUnreachableThreshold()) - notificationManager.post(NotificationId.PUMP_UNREACHABLE, R.string.pump_unreachable, sound = AlarmSound.ALARM) + notificationManager.post(NotificationId.PUMP_UNREACHABLE, TextRef.AndroidRes(R.string.pump_unreachable), sound = AlarmSound.ALARM) if (preferences.get(BooleanKey.NsClientCreateAnnouncementsFromErrors) && config.APS) appScope.launch { persistenceLayer.insertPumpTherapyEventIfNewByTimestamp( @@ -135,7 +136,7 @@ class LocalAlertUtilsImpl @Inject constructor( && preferences.get(LocalAlertLongKey.NextMissedReadingsAlarm) < dateUtil.now() ) { preferences.put(LocalAlertLongKey.NextMissedReadingsAlarm, dateUtil.now() + missedReadingsThreshold()) - notificationManager.post(NotificationId.BG_READINGS_MISSED, R.string.missed_bg_readings, sound = AlarmSound.ALARM) + notificationManager.post(NotificationId.BG_READINGS_MISSED, TextRef.AndroidRes(R.string.missed_bg_readings), sound = AlarmSound.ALARM) if (preferences.get(BooleanKey.NsClientCreateAnnouncementsFromErrors) && config.APS) { appScope.launch { persistenceLayer.insertPumpTherapyEventIfNewByTimestamp( diff --git a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt index 82cb8ef37ffc..c84cc47dc9a6 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/notifications/NotificationManagerImpl.kt @@ -10,6 +10,7 @@ import android.content.IntentFilter import android.media.AudioManager import android.media.RingtoneManager import android.os.Build +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.notifications.AlarmSound import androidx.annotation.StringRes import androidx.core.app.NotificationCompat @@ -207,8 +208,7 @@ class NotificationManagerImpl @Inject constructor( @Synchronized override fun post( id: NotificationId, - @StringRes textRes: Int, - vararg formatArgs: Any?, + textRef: TextRef, level: NotificationLevel, validMinutes: Int, date: Long, @@ -217,7 +217,7 @@ class NotificationManagerImpl @Inject constructor( actions: List, validityCheck: (() -> Boolean)? ): NotificationHandle { - val text = if (formatArgs.isEmpty()) rh.gs(textRes) else rh.gs(textRes, *formatArgs) + val text = rh.gs(textRef) val effectiveValidTo = if (validMinutes > 0) date + validMinutes.toLong().minutes.inWholeMilliseconds else validTo return postInternal( id = id, text = text, level = level, diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt index e95c62b5d34d..4df8bccd243f 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileRepositoryImpl.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.profile +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.data.Block @@ -580,8 +581,7 @@ class ProfileRepositoryImpl @Inject constructor( } else { notificationManager.post( NotificationId.INVALID_PROFILE_NOT_ACCEPTED, - R.string.invalid_profile_not_accepted, p.toString() - ) + TextRef.AndroidRes(R.string.invalid_profile_not_accepted, listOf(p.toString()))) } } if (newProfiles.isNotEmpty()) { diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt index 2d47b7fca03c..33039cabb17b 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.pump +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.data.model.CA import app.aaps.core.data.model.EB @@ -97,7 +98,7 @@ class PumpSyncImplementation @Inject constructor( } if (showNotification && (type.description != storedType || serialNumber != storedSerial) && timestamp >= storedTimestamp) - notificationManager.post(NotificationId.WRONG_PUMP_DATA, R.string.wrong_pump_data) + notificationManager.post(NotificationId.WRONG_PUMP_DATA, TextRef.AndroidRes(R.string.wrong_pump_data)) aapsLogger.error( LTag.PUMP, "Ignoring pump history record Allowed: ${dateUtil.dateAndTimeAndSecondsString(storedTimestamp)} $storedType $storedSerial Received: $timestamp ${ diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index 233864478aac..3ba01ddf9b44 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle +import app.aaps.core.keys.interfaces.TextRef import app.aaps.annotations.OpenForTesting import app.aaps.core.data.model.BS import app.aaps.core.data.model.EPS @@ -218,7 +219,7 @@ class CommandQueueImplementation @Inject constructor( } notificationManager.dismiss(NotificationId.FAILED_UPDATE_PROFILE) if (result.enacted && !silent) - notificationManager.post(NotificationId.PROFILE_SET_OK, rh.gs(app.aaps.core.ui.R.string.profile_set_ok), validMinutes = 60) + notificationManager.post(NotificationId.PROFILE_SET_OK, TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_set_ok), validMinutes = 60) return true } @@ -440,7 +441,7 @@ class CommandQueueImplementation @Inject constructor( aapsLogger.error(LTag.PUMPQUEUE, "Failed to store carbs after bolus", e) // The bolus succeeded but the carbs weren't persisted, so COB/IOB would be wrong and the // loss was previously silent (log-only). Alert the user so they can re-enter the carbs. - notificationManager.post(NotificationId.CARBS_STORE_FAILED, rh.gs(app.aaps.core.ui.R.string.carbs_not_saved_after_bolus)) + notificationManager.post(NotificationId.CARBS_STORE_FAILED, TextRef.AndroidRes(app.aaps.core.ui.R.string.carbs_not_saved_after_bolus)) } } return result @@ -588,7 +589,7 @@ class CommandQueueImplementation @Inject constructor( val basalValues = profile.getBasalValues() for (basalValue in basalValues) { if (basalValue.value < activePlugin.activePump.pumpDescription.basalMinimumRate) { - notificationManager.post(NotificationId.BASAL_VALUE_BELOW_MINIMUM, R.string.basal_value_below_minimum) + notificationManager.post(NotificationId.BASAL_VALUE_BELOW_MINIMUM, TextRef.AndroidRes(R.string.basal_value_below_minimum)) return pumpEnactResultProvider.get().success(false).enacted(false).comment(R.string.basal_value_below_minimum) } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index 441ea11e45e3..f478854644c2 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -3,6 +3,7 @@ package app.aaps.implementation.queue import android.content.Context import android.os.PowerManager import androidx.compose.ui.text.font.FontWeight +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.interfaces.alerts.LocalAlertUtils import app.aaps.core.interfaces.configuration.Config @@ -227,8 +228,8 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { // The user is alerted (URGENT) that the carbs were lost — not silently dropped. verify(notificationManager).post( - eq(NotificationId.CARBS_STORE_FAILED), eq("Carbs could not be saved"), any(), - any(), anyOrNull(), any>(), anyOrNull() + eq(NotificationId.CARBS_STORE_FAILED), eq(TextRef.AndroidRes(app.aaps.core.ui.R.string.carbs_not_saved_after_bolus)), + any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull() ) } @@ -264,9 +265,12 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { } // Both helpers match the String post() overload (id, text, level, validMinutes, sound, actions, validityCheck). - private fun verifyOkPosted(text: String) = + // PROFILE_SET_OK now passes the TextRef and lets the notification resolve it, so this matches the + // TextRef overload (id, textRef, level, validMinutes, date, validTo, sound, actions, validityCheck). + private fun verifyOkPosted() = verify(notificationManager).post( - eq(NotificationId.PROFILE_SET_OK), eq(text), any(), any(), + eq(NotificationId.PROFILE_SET_OK), eq(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_set_ok)), + any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull() ) @@ -288,7 +292,7 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { assertThat(persisted).isTrue() verify(notificationManager).dismiss(NotificationId.FAILED_UPDATE_PROFILE) - verifyOkPosted("Basal profile in pump updated") + verifyOkPosted() } @Test diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt index cc262b3cec66..469c7e7fecbd 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.aps.openAPSAutoISF import androidx.collection.LongSparseArray import androidx.collection.forEach +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.aps.SMBDefaults import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit @@ -169,8 +170,10 @@ open class OpenAPSAutoISFPlugin @Inject constructor( if (sensitivity.second == null && caller == "OpenAPSSMBPlugin") notificationManager.post( NotificationId.DYN_ISF_FALLBACK, - R.string.fallback_to_isf_no_tdd, sensitivity.first, level = NotificationLevel.INFO, date = start, validTo = dateUtil.now() + T.mins(1).msecs() - ) + TextRef.AndroidRes(R.string.fallback_to_isf_no_tdd, listOf(sensitivity.first)), + level = NotificationLevel.INFO, + date = start, + validTo = dateUtil.now() + T.mins(1).msecs()) else notificationManager.dismiss(NotificationId.DYN_ISF_FALLBACK) profiler.log(LTag.APS, String.format(Locale.getDefault(), "getIsfMgdl() %s %f %s %s", sensitivity.first, sensitivity.second, dateUtil.dateAndTimeAndSecondsString(start), caller), start) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index 6df1fdfd44ed..b07e7ce15460 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.aps.openAPSSMB import androidx.collection.LongSparseArray import androidx.collection.forEach +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.aps.SMBDefaults import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.plugin.PluginType @@ -149,8 +150,10 @@ open class OpenAPSSMBPlugin @Inject constructor( if (sensitivity.second == null) notificationManager.post( NotificationId.DYN_ISF_FALLBACK, - R.string.fallback_to_isf_no_tdd, sensitivity.first, level = NotificationLevel.INFO, date = start, validTo = dateUtil.now() + T.mins(1).msecs() - ) + TextRef.AndroidRes(R.string.fallback_to_isf_no_tdd, listOf(sensitivity.first)), + level = NotificationLevel.INFO, + date = start, + validTo = dateUtil.now() + T.mins(1).msecs()) else notificationManager.dismiss(NotificationId.DYN_ISF_FALLBACK) profiler.log(LTag.APS, "getIsfMgdl() multiplier=${multiplier} reason=${sensitivity.first} sensitivity=${sensitivity.second} caller=$caller", start) @@ -359,8 +362,9 @@ open class OpenAPSSMBPlugin @Inject constructor( if (dynIsfMode && !dynIsfResult.tddPartsCalculated()) { notificationManager.post( NotificationId.SMB_FALLBACK, - R.string.fallback_smb_no_tdd, level = NotificationLevel.INFO, validTo = dateUtil.now() + T.mins(1).msecs() - ) + TextRef.AndroidRes(R.string.fallback_smb_no_tdd), + level = NotificationLevel.INFO, + validTo = dateUtil.now() + T.mins(1).msecs()) inputConstraints.copyReasons( ConstraintObject(false, aapsLogger).also { it.set(false, rh.gs(R.string.fallback_smb_no_tdd), this) diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt index cc9e6cf505ce..7ee422466cee 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/RunningConfigurationImpl.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.configuration.configBuilder +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.SceneLifecycle import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.configuration.Config @@ -175,7 +176,7 @@ class RunningConfigurationImpl @Inject constructor( configuration.version?.let { nsClientRepository.addLog("◄ VERSION", "Received AAPS version $it") if (config.VERSION_NAME.startsWith(it).not()) - notificationManager.post(NotificationId.NSCLIENT_VERSION_DOES_NOT_MATCH, R.string.nsclient_version_does_not_match) + notificationManager.post(NotificationId.NSCLIENT_VERSION_DOES_NOT_MATCH, TextRef.AndroidRes(R.string.nsclient_version_does_not_match)) } // APS/Sensitivity/Smoothing/Calibration selection is adopted via the synced ActivePlugin* keys // (applied below in syncedPrefs → ConfigBuilder's key observer performs the switch). diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt index 847a38622e68..49b65db2c300 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt @@ -61,11 +61,10 @@ class DstHelperPlugin @Inject constructor( if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) { notificationManager.post( NotificationId.DST_IN_24H, - R.string.dst_in_24h_warning, + TextRef.AndroidRes(R.string.dst_in_24h_warning), actions = listOf(NotificationAction(TextRef.AndroidRes(app.aaps.core.ui.R.string.snooze)) { preferences.put(DstHelperLongKey.SnoozeDstIn24h, System.currentTimeMillis() + T.hours(24).msecs()) - }) - ) + })) } } if (wasDST(cal)) { @@ -81,11 +80,10 @@ class DstHelperPlugin @Inject constructor( if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) { notificationManager.post( NotificationId.DST_LOOP_DISABLED, - R.string.dst_loop_disabled_warning, + TextRef.AndroidRes(R.string.dst_loop_disabled_warning), actions = listOf(NotificationAction(TextRef.AndroidRes(app.aaps.core.ui.R.string.snooze)) { preferences.put(DstHelperLongKey.SnoozeLoopDisabled, System.currentTimeMillis() + T.hours(24).msecs()) - }) - ) + })) } } else { aapsLogger.debug(LTag.CONSTRAINTS, "Loop already suspended") diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt index df8e416f90a8..06f4c8a8ca2d 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt @@ -70,7 +70,7 @@ class SafetyPlugin @Inject constructor( override suspend fun isClosedLoopAllowed(value: Constraint): Constraint { if (!config.isEngineeringModeOrRelease()) { if (value.value()) { - notificationManager.post(NotificationId.TOAST_ALARM, R.string.closed_loop_disabled_on_dev_branch, level = NotificationLevel.NORMAL) + notificationManager.post(NotificationId.TOAST_ALARM, TextRef.AndroidRes(R.string.closed_loop_disabled_on_dev_branch), level = NotificationLevel.NORMAL) } value.set(false, rh.gs(R.string.closed_loop_disabled_on_dev_branch), this) } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt index 8afabb50cc0c..e5557a9e377b 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Handler import android.os.HandlerThread +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.PluginConstraints @@ -105,7 +106,7 @@ class SignatureVerifierPlugin @Inject constructor( } private fun showNotification() { - notificationManager.post(NotificationId.INVALID_VERSION, R.string.running_invalid_version) + notificationManager.post(NotificationId.INVALID_VERSION, TextRef.AndroidRes(R.string.running_invalid_version)) } private fun hasIllegalSignature(): Boolean { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt index cff7dce40951..58a4f18692ff 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.constraints.storage import android.os.Environment import android.os.StatFs +import app.aaps.core.keys.interfaces.TextRef import app.aaps.annotations.OpenForTesting import app.aaps.core.data.configuration.Constants import app.aaps.core.data.plugin.PluginType @@ -38,7 +39,7 @@ class StorageConstraintPlugin @Inject constructor( if (diskFree < Constants.MINIMUM_FREE_SPACE) { aapsLogger.debug(LTag.CONSTRAINTS, "Closed loop disabled. Internal storage free (Mb):$diskFree") value.set(false, rh.gs(R.string.disk_full, Constants.MINIMUM_FREE_SPACE), this) - notificationManager.post(NotificationId.DISK_FULL, R.string.disk_full, Constants.MINIMUM_FREE_SPACE) + notificationManager.post(NotificationId.DISK_FULL, TextRef.AndroidRes(R.string.disk_full, listOf(Constants.MINIMUM_FREE_SPACE))) } return value } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt index 00f831310839..b5e51ad05b0e 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerUtilsImpl.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.constraints.versionChecker import android.os.Build +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.time.T import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger @@ -116,7 +117,7 @@ class VersionCheckerUtilsImpl @Inject constructor( val now = dateUtil.now() if (dateUtil.isAfterNoon() && now > preferences.get(VersionCheckerLongKey.LastVersionCheckWarning) + warnEvery(0)) { aapsLogger.debug(LTag.CORE, "Version $currentVersion outdated. Found $newVersion") - notificationManager.post(NotificationId.NEW_VERSION_DETECTED, R.string.versionavailable, newVersion.toString(), level = NotificationLevel.LOW) + notificationManager.post(NotificationId.NEW_VERSION_DETECTED, TextRef.AndroidRes(R.string.versionavailable, listOf(newVersion.toString())), level = NotificationLevel.LOW) preferences.put(VersionCheckerLongKey.LastVersionCheckWarning, now) } return true @@ -128,10 +129,10 @@ class VersionCheckerUtilsImpl @Inject constructor( // store last notification time preferences.put(VersionCheckerLongKey.LastVersionCheckWarning, now) //notify - notificationManager.post(NotificationId.VERSION_EXPIRE, R.string.application_expired) + notificationManager.post(NotificationId.VERSION_EXPIRE, TextRef.AndroidRes(R.string.application_expired)) } else if (dateUtil.isAfterNoon() && now > preferences.get(VersionCheckerLongKey.LastVersionCheckWarning) + warnEvery(endDate)) { aapsLogger.debug(LTag.CORE, rh.gs(R.string.version_expire, currentVersion, dateUtil.dateString(endDate))) - notificationManager.post(NotificationId.VERSION_EXPIRE, R.string.version_expire, currentVersion, dateUtil.dateString(endDate), level = NotificationLevel.LOW) + notificationManager.post(NotificationId.VERSION_EXPIRE, TextRef.AndroidRes(R.string.version_expire, listOf(currentVersion,dateUtil.dateString(endDate))), level = NotificationLevel.LOW) preferences.put(VersionCheckerLongKey.LastExpiredWarning, now) } } diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt index fd5d761e0e91..9d28956cf2f9 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt @@ -6,6 +6,7 @@ import androidx.annotation.VisibleForTesting import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor @@ -158,7 +159,7 @@ class AidexPlugin @Inject constructor( sensorExpiredNotified = true notificationManager.post( id = NotificationId.AIDEX_SENSOR_EXPIRED, - textRes = R.string.aidex_sensor_expired, + textRef = TextRef.AndroidRes(R.string.aidex_sensor_expired), level = NotificationLevel.IMPORTANT, validMinutes = 60 ) @@ -174,7 +175,7 @@ class AidexPlugin @Inject constructor( replaceSensorNotified = true notificationManager.post( id = NotificationId.AIDEX_REPLACE_SENSOR, - textRes = R.string.aidex_sensor_replace, + textRef = TextRef.AndroidRes(R.string.aidex_sensor_replace), level = NotificationLevel.NORMAL, validMinutes = 120 ) @@ -190,7 +191,7 @@ class AidexPlugin @Inject constructor( sensorErrorNotified = true notificationManager.post( id = NotificationId.AIDEX_SENSOR_ERROR, - textRes = R.string.aidex_sensor_error, + textRef = TextRef.AndroidRes(R.string.aidex_sensor_error), level = NotificationLevel.IMPORTANT, validMinutes = 60 ) @@ -206,7 +207,7 @@ class AidexPlugin @Inject constructor( sensorStablingNotified = true notificationManager.post( id = NotificationId.AIDEX_SENSOR_STABILIZING, - textRes = R.string.aidex_sensor_stabilizing, + textRef = TextRef.AndroidRes(R.string.aidex_sensor_stabilizing), level = NotificationLevel.NORMAL, validMinutes = 60 ) @@ -222,7 +223,7 @@ class AidexPlugin @Inject constructor( signalLostNotified = true notificationManager.post( id = NotificationId.AIDEX_SIGNAL_LOST, - textRes = R.string.aidex_signal_lost, + textRef = TextRef.AndroidRes(R.string.aidex_signal_lost), level = NotificationLevel.NORMAL, validMinutes = 30 ) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetector.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetector.kt index 76ccfa4519f1..899a52c3784f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetector.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetector.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.nsclientV3.clientcontrol +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger @@ -109,7 +110,7 @@ class OrphanDetector @Inject constructor( } _authorized.value = false aapsLogger.warn(LTag.NSCLIENT, "ClientControl: clientId=${pairing.clientId} not in master's authorizedClients — orphan") - notificationManager.post(NotificationId.NSCLIENT_PAIRING_ORPHAN, rh.gs(R.string.clientcontrol_orphan_notification)) + notificationManager.post(NotificationId.NSCLIENT_PAIRING_ORPHAN, TextRef.AndroidRes(R.string.clientcontrol_orphan_notification)) } companion object { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index 6f123dee2452..0a1cdf00b8bd 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -1067,10 +1067,10 @@ class SmsCommunicatorPlugin @Inject constructor( messages.add(sms) } catch (e: IllegalArgumentException) { return if (e.message == "Invalid message body") { - notificationManager.post(NotificationId.INVALID_MESSAGE_BODY, R.string.smscommunicator_message_body) + notificationManager.post(NotificationId.INVALID_MESSAGE_BODY, TextRef.AndroidRes(R.string.smscommunicator_message_body)) false } else { - notificationManager.post(NotificationId.INVALID_PHONE_NUMBER, R.string.smscommunicator_invalid_phone_number) + notificationManager.post(NotificationId.INVALID_PHONE_NUMBER, TextRef.AndroidRes(R.string.smscommunicator_invalid_phone_number)) false } } catch (_: SecurityException) { diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetectorTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetectorTest.kt index 91d1f467df16..964d3975f72e 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetectorTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/clientcontrol/OrphanDetectorTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.nsclientV3.clientcontrol +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.notifications.NotificationAction @@ -85,7 +86,7 @@ internal class OrphanDetectorTest { @Test fun rosterMissingUsOutsideRaceWindowFiresOrphanNotification() { sut.onSettingsDoc(configWithRoster("stranger"), docSrvModified = now) - verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) + verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) } /** @@ -109,14 +110,14 @@ internal class OrphanDetectorTest { pairedAt = now - 2 * 60_000L val docSrvModified = now sut.onSettingsDoc(configWithRoster("stranger"), docSrvModified = docSrvModified) - verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) + verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) } /** Empty roster = master has zero authorized clients (typical post-reinstall). Treat as orphan. */ @Test fun emptyRosterFires() { sut.onSettingsDoc(configWithRoster(), docSrvModified = now) - verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) + verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) } /** Master device must never alarm itself. */ @@ -146,7 +147,7 @@ internal class OrphanDetectorTest { fun missingSrvModifiedSkipsRaceGuardButStillFiresIfPairedAtIsZero() { pairedAt = 0L // legacy install: never set pairedAt sut.onSettingsDoc(configWithRoster("stranger"), docSrvModified = 0L) - verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) + verify(notificationManager).post(eq(NotificationId.NSCLIENT_PAIRING_ORPHAN), any(), any(), any(), any(), any(), anyOrNull(), any>(), anyOrNull()) } // ---- authorized StateFlow (folded into NsClient.masterReachable to gate a revoked client's edits) ---- diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index 3d52198a2e31..fb957e8db527 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -334,7 +334,7 @@ class ComboV2Plugin @Inject constructor( pumpManager = newPumpManager } catch (_: BluetoothNotAvailableException) { - notificationManager.post(NotificationId.BLUETOOTH_NOT_SUPPORTED, R.string.combov2_bluetooth_not_supported) + notificationManager.post(NotificationId.BLUETOOTH_NOT_SUPPORTED, TextRef.AndroidRes(R.string.combov2_bluetooth_not_supported)) // Deliberately _not_ setting the driver state here before // exiting this scope. We are essentially aborting the start @@ -344,7 +344,7 @@ class ComboV2Plugin @Inject constructor( aapsLogger.error(LTag.PUMP, "combov2 driver start cannot be completed since the hardware does not support Bluetooth") return@runWithPermissionCheck } catch (_: BluetoothNotEnabledException) { - notificationManager.post(NotificationId.BLUETOOTH_NOT_ENABLED, R.string.combov2_bluetooth_disabled) + notificationManager.post(NotificationId.BLUETOOTH_NOT_ENABLED, TextRef.AndroidRes(R.string.combov2_bluetooth_disabled)) // If the user currently has Bluetooth disabled, retry until // the user turns it on. AAPS will automatically show a dialog @@ -491,7 +491,7 @@ class ComboV2Plugin @Inject constructor( if (pumpErrorObserved) { aapsLogger.debug(LTag.PUMP, "Aborting connect attempt since the pumpErrorObserved flag is set") - notificationManager.post(NotificationId.COMBO_PUMP_ALARM, R.string.combov2_cannot_connect_pump_error_observed, level = NotificationLevel.NORMAL) + notificationManager.post(NotificationId.COMBO_PUMP_ALARM, TextRef.AndroidRes(R.string.combov2_cannot_connect_pump_error_observed), level = NotificationLevel.NORMAL) return } @@ -660,8 +660,7 @@ class ComboV2Plugin @Inject constructor( if ((activeBasalProfileNumber != null) && (activeBasalProfileNumber != 1)) { notificationManager.post( NotificationId.COMBO_PUMP_ALARM, - R.string.combov2_incorrect_active_basal_profile, activeBasalProfileNumber - ) + TextRef.AndroidRes(R.string.combov2_incorrect_active_basal_profile, listOf(activeBasalProfileNumber))) } lastActiveBasalProfileNumber = activeBasalProfileNumber } @@ -693,8 +692,7 @@ class ComboV2Plugin @Inject constructor( } catch (e: Exception) { notificationManager.post( NotificationId.COMBO_PUMP_ALARM, - R.string.combov2_connection_error, e.message - ) + TextRef.AndroidRes(R.string.combov2_connection_error, listOf(e.message.toString()))) aapsLogger.error(LTag.PUMP, "Exception while connecting: ${e.stackTraceToString()}") @@ -730,7 +728,7 @@ class ComboV2Plugin @Inject constructor( } } } catch (_: BluetoothNotEnabledException) { - notificationManager.post(NotificationId.BLUETOOTH_NOT_ENABLED, R.string.combov2_bluetooth_disabled) + notificationManager.post(NotificationId.BLUETOOTH_NOT_ENABLED, TextRef.AndroidRes(R.string.combov2_bluetooth_disabled)) } catch (e: Exception) { aapsLogger.error(LTag.PUMP, "Connection failure: $e") rxBus.send(EventShowSnackbar(rh.gs(R.string.combov2_could_not_connect), EventShowSnackbar.Type.Error)) @@ -1769,11 +1767,11 @@ class ComboV2Plugin @Inject constructor( when (event) { is ComboCtlPump.Event.BatteryLow -> { - notificationManager.post(NotificationId.COMBO_PUMP_ALARM, R.string.combov2_battery_low_warning, level = NotificationLevel.NORMAL) + notificationManager.post(NotificationId.COMBO_PUMP_ALARM, TextRef.AndroidRes(R.string.combov2_battery_low_warning), level = NotificationLevel.NORMAL) } is ComboCtlPump.Event.ReservoirLow -> { - notificationManager.post(NotificationId.COMBO_PUMP_ALARM, R.string.combov2_reservoir_low_warning, level = NotificationLevel.NORMAL) + notificationManager.post(NotificationId.COMBO_PUMP_ALARM, TextRef.AndroidRes(R.string.combov2_reservoir_low_warning), level = NotificationLevel.NORMAL) } is ComboCtlPump.Event.QuickBolusInfused -> { @@ -1879,11 +1877,8 @@ class ComboV2Plugin @Inject constructor( ) notificationManager.post( NotificationId.COMBO_UNKNOWN_TBR, - R.string.combov2_unknown_tbr_detected, - event.tbrPercentage, - remainingDurationString, - level = NotificationLevel.IMPORTANT - ) + TextRef.AndroidRes(R.string.combov2_unknown_tbr_detected, listOf(event.tbrPercentage, remainingDurationString)), + level = NotificationLevel.IMPORTANT) } else -> Unit @@ -2033,7 +2028,7 @@ class ComboV2Plugin @Inject constructor( // that the Combo is currently suspended, otherwise this // only shows up in the Combo fragment. if (newState == DriverState.Suspended) { - notificationManager.post(NotificationId.PUMP_SUSPENDED, R.string.combov2_pump_is_suspended) + notificationManager.post(NotificationId.PUMP_SUSPENDED, TextRef.AndroidRes(R.string.combov2_pump_is_suspended)) } } @@ -2072,7 +2067,7 @@ class ComboV2Plugin @Inject constructor( private fun unpairDueToPumpDataError() { disconnectInternal(forceDisconnect = true) - notificationManager.post(NotificationId.PUMP_ERROR, R.string.combov2_cannot_access_pump_data, date = dateUtil.now(), validTo = 0) + notificationManager.post(NotificationId.PUMP_ERROR, TextRef.AndroidRes(R.string.combov2_cannot_access_pump_data), date = dateUtil.now(), validTo = 0) unpair() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusBolus.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusBolus.kt index 6ec931eb0f2d..6060172a4f71 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusBolus.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusBolus.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danar.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId import dagger.android.HasAndroidInjector @@ -28,7 +29,7 @@ class MsgInitConnStatusBolus( aapsLogger.debug(LTag.PUMPCOMM, "Bolus increment: " + danaPump.bolusStep) aapsLogger.debug(LTag.PUMPCOMM, "Bolus max: " + danaPump.maxBolus) if (!danaPump.isExtendedBolusEnabled) { - notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, app.aaps.pump.dana.R.string.danar_enableextendedbolus) + notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_enableextendedbolus)) } else { notificationManager.dismiss(NotificationId.EXTENDED_BOLUS_DISABLED) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusOption.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusOption.kt index 819d4136686c..65e82760603a 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusOption.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusOption.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danar.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId import dagger.android.HasAndroidInjector @@ -31,7 +32,7 @@ class MsgInitConnStatusOption( failed = true } if (!danaPump.isPasswordOK) { - notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, app.aaps.pump.dana.R.string.wrongpumppassword) + notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, TextRef.AndroidRes(app.aaps.pump.dana.R.string.wrongpumppassword)) } else { notificationManager.dismiss(NotificationId.WRONG_PUMP_PASSWORD) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusTime.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusTime.kt index 57e8ee6e0770..072de9c08b2e 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusTime.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgInitConnStatusTime.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danar.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -17,7 +18,7 @@ class MsgInitConnStatusTime( override fun handleMessage(bytes: ByteArray) { if (bytes.size - 10 > 7) { - notificationManager.post(NotificationId.WRONG_DRIVER, app.aaps.pump.dana.R.string.pumpdrivercorrected) + notificationManager.post(NotificationId.WRONG_DRIVER, TextRef.AndroidRes(app.aaps.pump.dana.R.string.pumpdrivercorrected)) danaRPlugin.disconnect("Wrong Model") aapsLogger.debug(LTag.PUMPCOMM, "Wrong model selected. Switching to Korean DanaR") danaRKoreanPlugin.setPluginEnabled(PluginType.PUMP, true) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgSettingMeal.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgSettingMeal.kt index 757066cce85c..d5ee25923a23 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgSettingMeal.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/comm/MsgSettingMeal.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danar.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationLevel @@ -32,12 +33,12 @@ class MsgSettingMeal( danaPump.basalStep = 0.01 } if (danaPump.basalStep != 0.01) { - notificationManager.post(NotificationId.WRONG_BASAL_STEP, app.aaps.pump.dana.R.string.danar_setbasalstep001, level = NotificationLevel.IMPORTANT) + notificationManager.post(NotificationId.WRONG_BASAL_STEP, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_setbasalstep001), level = NotificationLevel.IMPORTANT) } else { notificationManager.dismiss(NotificationId.WRONG_BASAL_STEP) } if (danaPump.isConfigUD) { - notificationManager.post(NotificationId.UD_MODE_ENABLED, app.aaps.pump.dana.R.string.danar_switchtouhmode) + notificationManager.post(NotificationId.UD_MODE_ENABLED, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_switchtouhmode)) } else { notificationManager.dismiss(NotificationId.UD_MODE_ENABLED) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt index da7c031f33ec..2f8f2d73369c 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.IBinder import android.os.SystemClock +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.di.ApplicationScope import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag @@ -266,7 +267,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { if (temporaryBasal.rate != danaPump.tempBasalPercent.toDouble() || abs(temporaryBasal.timestamp - danaPump.tempBasalStart) > 10000 ) { // Close current temp basal - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Different temporary basal found running AAPS: " + (temporaryBasal.toString() + " DanaPump " + danaPump.temporaryBasalToString())) pumpSync.syncTemporaryBasalWithPumpId( danaPump.tempBasalStart, @@ -286,7 +287,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { activePlugin.activePump.model(), activePlugin.activePump.serialNumber() ) - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Temporary basal should not be running. Sending stop to AAPS") } } else { @@ -301,7 +302,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { activePlugin.activePump.model(), activePlugin.activePump.serialNumber() ) - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Temporary basal should be running: DanaPump " + danaPump.temporaryBasalToString()) } } @@ -311,7 +312,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { if (extendedBolus.rate != danaPump.extendedBolusAbsoluteRate || abs(extendedBolus.timestamp - danaPump.extendedBolusStart) > 10000 ) { // Close current extended - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Different extended bolus found running AAPS: " + (extendedBolus.toString() + " DanaPump " + danaPump.extendedBolusToString())) pumpSync.syncExtendedBolusWithPumpId( danaPump.extendedBolusStart, @@ -330,12 +331,12 @@ abstract class AbstractDanaRExecutionService : DaggerService() { activePlugin.activePump.model(), activePlugin.activePump.serialNumber() ) - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Extended bolus should not be running. Sending stop to AAPS") } } else { if (danaPump.isExtendedInProgress) { // Create new - notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, app.aaps.pump.danar.R.string.unsupported_action_in_pump) + notificationManager.post(NotificationId.UNSUPPORTED_ACTION_IN_PUMP, TextRef.AndroidRes(app.aaps.pump.danar.R.string.unsupported_action_in_pump)) aapsLogger.error(LTag.PUMP, "Extended bolus should not be running: DanaPump " + danaPump.extendedBolusToString()) pumpSync.syncExtendedBolusWithPumpId( danaPump.extendedBolusStart, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt index 03b40f862b3a..052699d0d4b0 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/DanaRExecutionService.kt @@ -136,7 +136,7 @@ class DanaRExecutionService : AbstractDanaRExecutionService() { if (danaPump.dailyTotalUnits > danaPump.maxDailyTotalUnits * Constants.DAILY_RESERVOIR_LIMIT_WARNING) { aapsLogger.debug(LTag.PUMP, "Approaching daily limit: " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits) if (System.currentTimeMillis() > lastApproachingDailyLimit + 30 * 60 * 1000) { - notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, R.string.approachingdailylimit) + notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, TextRef.AndroidRes(R.string.approachingdailylimit)) pumpSync.insertAnnouncement( rh.gs(R.string.approachingdailylimit) + ": " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits + "U", null, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBasicK.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBasicK.kt index 4cb1e69eebad..b119b4df4107 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBasicK.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBasicK.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danarkorean.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.pump.danar.comm.MessageBase @@ -29,12 +30,12 @@ class MsgInitConnStatusBasicK( aapsLogger.debug(LTag.PUMPCOMM, "easyUIMode: $easyUIMode") aapsLogger.debug(LTag.PUMPCOMM, "Pump password: " + danaPump.password) if (danaPump.isEasyModeEnabled) { - notificationManager.post(NotificationId.EASY_MODE_ENABLED, app.aaps.pump.dana.R.string.danar_disableeasymode) + notificationManager.post(NotificationId.EASY_MODE_ENABLED, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_disableeasymode)) } else { notificationManager.dismiss(NotificationId.EASY_MODE_ENABLED) } if (!danaPump.isPasswordOK) { - notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, app.aaps.pump.dana.R.string.wrongpumppassword) + notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, TextRef.AndroidRes(app.aaps.pump.dana.R.string.wrongpumppassword)) } else { notificationManager.dismiss(NotificationId.WRONG_PUMP_PASSWORD) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBolusK.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBolusK.kt index 504ad2d1426a..b343b2c36649 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBolusK.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusBolusK.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danarkorean.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.pump.danar.comm.MessageBase @@ -29,7 +30,7 @@ class MsgInitConnStatusBolusK( aapsLogger.debug(LTag.PUMPCOMM, "Bolus max: " + danaPump.maxBolus) aapsLogger.debug(LTag.PUMPCOMM, "Delivery status: $deliveryStatus") if (!danaPump.isExtendedBolusEnabled) { - notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, app.aaps.pump.dana.R.string.danar_enableextendedbolus) + notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_enableextendedbolus)) } else { notificationManager.dismiss(NotificationId.EXTENDED_BOLUS_DISABLED) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusTimeK.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusTimeK.kt index 59c4488a53bf..1cdf0719ef55 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusTimeK.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/comm/MsgInitConnStatusTimeK.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danarkorean.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -18,7 +19,7 @@ class MsgInitConnStatusTimeK( override fun handleMessage(bytes: ByteArray) { if (bytes.size - 10 < 10) { - notificationManager.post(NotificationId.WRONG_DRIVER, app.aaps.pump.dana.R.string.pumpdrivercorrected) + notificationManager.post(NotificationId.WRONG_DRIVER, TextRef.AndroidRes(app.aaps.pump.dana.R.string.pumpdrivercorrected)) danaRKoreanPlugin.disconnect("Wrong Model") aapsLogger.debug(LTag.PUMPCOMM, "Wrong model selected. Switching to export DanaR") danaRKoreanPlugin.setPluginEnabled(PluginType.PUMP, false) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt index c4eed5133a17..97bc870abb24 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/services/DanaRKoreanExecutionService.kt @@ -128,7 +128,7 @@ class DanaRKoreanExecutionService : AbstractDanaRExecutionService() { if (danaPump.dailyTotalUnits > danaPump.maxDailyTotalUnits * Constants.DAILY_RESERVOIR_LIMIT_WARNING) { aapsLogger.debug(LTag.PUMP, "Approaching daily limit: " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits) if (System.currentTimeMillis() > lastApproachingDailyLimit + 30 * 60 * 1000) { - notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, R.string.approachingdailylimit) + notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, TextRef.AndroidRes(R.string.approachingdailylimit)) pumpSync.insertAnnouncement( rh.gs(R.string.approachingdailylimit) + ": " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits + "U", null, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/comm/MsgCheckValueV2.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/comm/MsgCheckValueV2.kt index a612882968e9..52a1553234c9 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/comm/MsgCheckValueV2.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/comm/MsgCheckValueV2.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danarv2.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -24,7 +25,7 @@ class MsgCheckValueV2( danaPump.protocol = intFromBuff(bytes, 1, 1) danaPump.productCode = intFromBuff(bytes, 2, 1) if (danaPump.hwModel != DanaPump.EXPORT_MODEL) { - notificationManager.post(NotificationId.WRONG_DRIVER, app.aaps.pump.dana.R.string.pumpdrivercorrected) + notificationManager.post(NotificationId.WRONG_DRIVER, TextRef.AndroidRes(app.aaps.pump.dana.R.string.pumpdrivercorrected)) danaRPlugin.disconnect("Wrong Model") aapsLogger.debug(LTag.PUMPCOMM, "Wrong model selected. Switching to Korean DanaR") danaRKoreanPlugin.setPluginEnabled(PluginType.PUMP, true) @@ -38,7 +39,7 @@ class MsgCheckValueV2( return } if (danaPump.protocol != 2) { - notificationManager.post(NotificationId.WRONG_DRIVER, app.aaps.pump.dana.R.string.pumpdrivercorrected) + notificationManager.post(NotificationId.WRONG_DRIVER, TextRef.AndroidRes(app.aaps.pump.dana.R.string.pumpdrivercorrected)) danaRKoreanPlugin.disconnect("Wrong Model") aapsLogger.debug(LTag.PUMPCOMM, "Wrong model selected. Switching to non APS DanaR") danaRv2Plugin.setPluginEnabled(PluginType.PUMP, false) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt index 8990a981ba33..50563587c00a 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/services/DanaRv2ExecutionService.kt @@ -159,7 +159,7 @@ class DanaRv2ExecutionService : AbstractDanaRExecutionService() { if (danaPump.dailyTotalUnits > danaPump.maxDailyTotalUnits * Constants.DAILY_RESERVOIR_LIMIT_WARNING) { aapsLogger.debug(LTag.PUMP, "Approaching daily limit: " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits) if (System.currentTimeMillis() > lastApproachingDailyLimit + 30 * 60 * 1000) { - notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, R.string.approachingdailylimit) + notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, TextRef.AndroidRes(R.string.approachingdailylimit)) pumpSync.insertAnnouncement( rh.gs(R.string.approachingdailylimit) + ": " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits + "U", null, diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBasalGetBasalRate.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBasalGetBasalRate.kt index caea699b566e..22b300a3e616 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBasalGetBasalRate.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBasalGetBasalRate.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danars.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -42,7 +43,7 @@ class DanaRSPacketBasalGetBasalRate @Inject constructor( aapsLogger.debug(LTag.PUMPCOMM, "Basal " + String.format(Locale.ENGLISH, "%02d", index) + "h: " + danaPump.pumpProfiles!![danaPump.activeProfile][index]) if (danaPump.basalStep != 0.01) { failed = true - notificationManager.post(NotificationId.WRONG_BASAL_STEP, app.aaps.pump.dana.R.string.danar_setbasalstep001) + notificationManager.post(NotificationId.WRONG_BASAL_STEP, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_setbasalstep001)) } else { notificationManager.dismiss(NotificationId.WRONG_BASAL_STEP) } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusGetBolusOption.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusGetBolusOption.kt index 6ad5efb9b2eb..97e0c1112d7c 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusGetBolusOption.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketBolusGetBolusOption.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danars.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -78,7 +79,7 @@ class DanaRSPacketBolusGetBolusOption @Inject constructor( dataSize = 1 val missedBolus04EndMin = byteArrayToInt(getBytes(data, dataIndex, dataSize)) if (!danaPump.isExtendedBolusEnabled) { - notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, app.aaps.pump.dana.R.string.danar_enableextendedbolus) + notificationManager.post(NotificationId.EXTENDED_BOLUS_DISABLED, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_enableextendedbolus)) failed = true } else { notificationManager.dismiss(NotificationId.EXTENDED_BOLUS_DISABLED) diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralGetPumpCheck.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralGetPumpCheck.kt index 15b82edbc633..be23aac3a76a 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralGetPumpCheck.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralGetPumpCheck.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danars.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -38,7 +39,7 @@ class DanaRSPacketGeneralGetPumpCheck @Inject constructor( aapsLogger.debug(LTag.PUMPCOMM, "Protocol: " + String.format("%02X ", danaPump.protocol)) aapsLogger.debug(LTag.PUMPCOMM, "Product Code: " + String.format("%02X ", danaPump.productCode)) if (danaPump.productCode < 2) { - notificationManager.post(NotificationId.UNSUPPORTED_FIRMWARE, app.aaps.pump.dana.R.string.unsupportedfirmware) + notificationManager.post(NotificationId.UNSUPPORTED_FIRMWARE, TextRef.AndroidRes(app.aaps.pump.dana.R.string.unsupportedfirmware)) } } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralInitialScreenInformation.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralInitialScreenInformation.kt index 2517490b07c2..d733d0002f2b 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralInitialScreenInformation.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/comm/DanaRSPacketGeneralInitialScreenInformation.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danars.comm +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationId @@ -50,7 +51,7 @@ class DanaRSPacketGeneralInitialScreenInformation @Inject constructor( // configuration for AAPS (no insulin can be delivered and the loop gets suspended), // so raise an urgent notification asking the user to disable it. if (danaPump.errorState == DanaPump.ErrorState.BOLUS_BLOCK) - notificationManager.post(NotificationId.DANA_BOLUS_BLOCK, app.aaps.pump.dana.R.string.danar_disablebolusblock) + notificationManager.post(NotificationId.DANA_BOLUS_BLOCK, TextRef.AndroidRes(app.aaps.pump.dana.R.string.danar_disablebolusblock)) else notificationManager.dismiss(NotificationId.DANA_BOLUS_BLOCK) } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/BLEComm.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/BLEComm.kt index b0d63835e3b5..2a5f4543cb2c 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/BLEComm.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/BLEComm.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.pm.PackageManager import android.util.Base64 import androidx.core.app.ActivityCompat +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.configuration.ConfigBuilder import app.aaps.core.interfaces.logging.AAPSLogger @@ -447,7 +448,7 @@ class BLEComm @Inject constructor( val deviceName = connectDeviceName if (deviceName == null || deviceName == "") { - notificationManager.post(NotificationId.DEVICE_NOT_PAIRED, R.string.pairfirst) + notificationManager.post(NotificationId.DEVICE_NOT_PAIRED, TextRef.AndroidRes(R.string.pairfirst)) return } @@ -523,7 +524,7 @@ class BLEComm @Inject constructor( mSendQueue.clear() rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED, rh.gs(R.string.pumperror))) runBlocking { pumpSync.insertAnnouncement(rh.gs(R.string.pumperror), null, danaPump.pumpType(), danaPump.serialNumber) } - notificationManager.post(NotificationId.PUMP_ERROR, R.string.pumperror) + notificationManager.post(NotificationId.PUMP_ERROR, TextRef.AndroidRes(R.string.pumperror)) // response BUSY: error status } else if (decryptedBuffer.size == 6 && decryptedBuffer[2] == 'B'.code.toByte() && decryptedBuffer[3] == 'U'.code.toByte() && decryptedBuffer[4] == 'S'.code.toByte() && decryptedBuffer[5] == 'Y'.code.toByte()) { aapsLogger.debug(LTag.PUMPBTCOMM, "<<<<< " + "ENCRYPTION__PUMP_CHECK (BUSY)" + " " + DanaRSPacket.toHexString(decryptedBuffer)) @@ -537,7 +538,7 @@ class BLEComm @Inject constructor( mSendQueue.clear() rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED, rh.gs(app.aaps.core.ui.R.string.connection_error))) danaRSPlugin.clearPairing() - notificationManager.post(NotificationId.WRONG_SERIAL_NUMBER, app.aaps.core.ui.R.string.password_cleared) + notificationManager.post(NotificationId.WRONG_SERIAL_NUMBER, TextRef.AndroidRes(app.aaps.core.ui.R.string.password_cleared)) } } @@ -629,7 +630,7 @@ class BLEComm @Inject constructor( aapsLogger.debug(LTag.PUMPBTCOMM, "Pump user password: " + danaPump.rsPassword) if (!danaPump.isRSPasswordOK) { aapsLogger.error(LTag.PUMPBTCOMM, "Wrong pump password") - notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, R.string.wrongpumppassword) + notificationManager.post(NotificationId.WRONG_PUMP_PASSWORD, TextRef.AndroidRes(R.string.wrongpumppassword)) bleTransport.updatePairingState(PairingState(step = PairingStep.WAITING_FOR_PASSWORD)) disconnect("WrongPassword") } else { diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt index 3038f6cb2052..805f49c9289e 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt @@ -288,7 +288,7 @@ class DanaRSService : DaggerService() { if (danaPump.dailyTotalUnits > danaPump.maxDailyTotalUnits * Constants.DAILY_RESERVOIR_LIMIT_WARNING) { aapsLogger.debug(LTag.PUMPCOMM, "Approaching daily limit: " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits) if (System.currentTimeMillis() > lastApproachingDailyLimit + 30 * 60 * 1000) { - notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, R.string.approachingdailylimit) + notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, TextRef.AndroidRes(R.string.approachingdailylimit)) pumpSync.insertAnnouncement( rh.gs(R.string.approachingdailylimit) + ": " + danaPump.dailyTotalUnits + "/" + danaPump.maxDailyTotalUnits + "U", null, diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt index 0f6b24aea11d..70cb9ec4d689 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt @@ -269,7 +269,7 @@ class DiaconnG8Service : DaggerService() { if (diaconnG8Pump.dailyTotalUnits > diaconnG8Pump.maxDailyTotalUnits * Constants.DAILY_RESERVOIR_LIMIT_WARNING) { aapsLogger.debug(LTag.PUMPCOMM, "Approaching daily limit: " + diaconnG8Pump.dailyTotalUnits + "/" + diaconnG8Pump.maxDailyTotalUnits) if (System.currentTimeMillis() > lastApproachingDailyLimit + 30 * 60 * 1000) { - notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, R.string.approachingdailylimit) + notificationManager.post(NotificationId.APPROACHING_DAILY_LIMIT, TextRef.AndroidRes(R.string.approachingdailylimit)) pumpSync.insertAnnouncement( rh.gs(R.string.approachingdailylimit) + ": " + diaconnG8Pump.dailyTotalUnits + "/" + diaconnG8Pump.maxDailyTotalUnits + "U", null, diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt index 2039ba05a9cd..5159595a6081 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import android.os.SystemClock +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.data.model.TE import app.aaps.core.data.plugin.PluginType @@ -351,7 +352,7 @@ class InsightPlugin @Inject constructor( val setDateTimeMessage = SetDateTimeMessage() setDateTimeMessage.pumpTime = pumpTime connectionService?.requestMessage(setDateTimeMessage)?.await() - notificationManager.post(NotificationId.INSIGHT_DATE_TIME_UPDATED, app.aaps.core.ui.R.string.pump_time_updated, validMinutes = 60) + notificationManager.post(NotificationId.INSIGHT_DATE_TIME_UPDATED, TextRef.AndroidRes(app.aaps.core.ui.R.string.pump_time_updated), validMinutes = 60) } } } @@ -1578,7 +1579,7 @@ class InsightPlugin @Inject constructor( } override fun onTimeoutDuringHandshake() { - notificationManager.post(NotificationId.INSIGHT_TIMEOUT_DURING_HANDSHAKE, R.string.timeout_during_handshake, level = NotificationLevel.IMPORTANT) + notificationManager.post(NotificationId.INSIGHT_TIMEOUT_DURING_HANDSHAKE, TextRef.AndroidRes(R.string.timeout_during_handshake), level = NotificationLevel.IMPORTANT) } override fun canHandleDST(): Boolean { diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index e017f2859ab0..c698135ecafb 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.ServiceConnection import android.os.IBinder import android.os.SystemClock +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType @@ -603,7 +604,7 @@ class MedtronicPumpPlugin @Inject constructor( aapsLogger.info(LTag.PUMP, String.format(Locale.ENGLISH, "MedtronicPumpPlugin::checkTimeAndOptionallySetTime - Time difference is %d s. Set time on pump.", timeDiff)) rileyLinkMedtronicService?.medtronicUIComm?.executeCommand(MedtronicCommandType.SetRealTimeClock) if (clock.timeDifference == 0) { - notificationManager.post(NotificationId.INSIGHT_DATE_TIME_UPDATED, app.aaps.core.ui.R.string.pump_time_updated, validMinutes = 60) + notificationManager.post(NotificationId.INSIGHT_DATE_TIME_UPDATED, TextRef.AndroidRes(app.aaps.core.ui.R.string.pump_time_updated), validMinutes = 60) } } else { if (clock.localDeviceTime.year > 2015) { diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/data/MedtronicHistoryData.kt index ac5797087ae4..b82e9701a486 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/data/MedtronicHistoryData.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/data/MedtronicHistoryData.kt @@ -1,5 +1,6 @@ package app.aaps.pump.medtronic.data +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.TE import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.logging.AAPSLogger @@ -781,7 +782,7 @@ class MedtronicHistoryData @Inject constructor( ) if (tempBasalProcessDTO.durationAsSeconds <= 0) { - notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, R.string.invalid_history_data, level = NotificationLevel.IMPORTANT) + notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, TextRef.AndroidRes(R.string.invalid_history_data), level = NotificationLevel.IMPORTANT) aapsLogger.debug(LTag.PUMP, "syncTemporaryBasalWithPumpId - Skipped") } else { val result = runBlocking { @@ -825,7 +826,7 @@ class MedtronicHistoryData @Inject constructor( ) if (tempBasalProcessDTO.durationAsSeconds <= 0) { - notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, R.string.invalid_history_data, level = NotificationLevel.IMPORTANT) + notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, TextRef.AndroidRes(R.string.invalid_history_data), level = NotificationLevel.IMPORTANT) aapsLogger.debug(LTag.PUMP, "syncTemporaryBasalWithPumpId - Skipped") } else { val result = runBlocking { @@ -1097,7 +1098,7 @@ class MedtronicHistoryData @Inject constructor( ) if (tempBasalProcess.durationAsSeconds <= 0) { - notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, R.string.invalid_history_data, level = NotificationLevel.IMPORTANT) + notificationManager.post(NotificationId.MDT_INVALID_HISTORY_DATA, TextRef.AndroidRes(R.string.invalid_history_data), level = NotificationLevel.IMPORTANT) aapsLogger.debug(LTag.PUMP, "syncTemporaryBasalWithPumpId - Skipped") } else { val result = runBlocking { diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/util/MedtronicUtil.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/util/MedtronicUtil.kt index 4e53b4161b9e..dd99bd563320 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/util/MedtronicUtil.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/util/MedtronicUtil.kt @@ -1,5 +1,6 @@ package app.aaps.pump.medtronic.util +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.NotificationManager @@ -98,7 +99,11 @@ class MedtronicUtil @Inject constructor( // } fun sendNotification(notificationType: MedtronicNotificationType, vararg parameters: Any?) { - notificationManager.post(notificationType.notificationId, notificationType.resourceId, *parameters, level = notificationType.notificationLevel) + notificationManager.post( + notificationType.notificationId, + TextRef.AndroidRes(notificationType.resourceId, parameters.filterNotNull()), + level = notificationType.notificationLevel + ) } fun dismissNotification(notificationType: MedtronicNotificationType) { diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt index 2cc7a67ca4ca..d1285fad5276 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt @@ -151,8 +151,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (medtrumPlugin.isInitialized() && !r.success) { notificationManager.post( NotificationId.PUMP_SETTINGS_FAILED, - R.string.pump_setting_failed, - ) + TextRef.AndroidRes(R.string.pump_setting_failed)) } } preferences.observe(MedtrumBooleanKey.MedtrumPatchExpiration).drop(1).collectResilient(scope, aapsLogger, LTag.PUMP) { @@ -161,8 +160,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (medtrumPlugin.isInitialized() && !r.success) { notificationManager.post( NotificationId.PUMP_SETTINGS_FAILED, - R.string.pump_setting_failed, - ) + TextRef.AndroidRes(R.string.pump_setting_failed)) } } preferences.observe(MedtrumIntKey.MedtrumHourlyMaxInsulin).drop(1).collectResilient(scope, aapsLogger, LTag.PUMP) { @@ -171,8 +169,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (medtrumPlugin.isInitialized() && !r.success) { notificationManager.post( NotificationId.PUMP_SETTINGS_FAILED, - R.string.pump_setting_failed, - ) + TextRef.AndroidRes(R.string.pump_setting_failed)) } } preferences.observe(MedtrumIntKey.MedtrumDailyMaxInsulin).drop(1).collectResilient(scope, aapsLogger, LTag.PUMP) { @@ -181,8 +178,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (medtrumPlugin.isInitialized() && !r.success) { notificationManager.post( NotificationId.PUMP_SETTINGS_FAILED, - R.string.pump_setting_failed, - ) + TextRef.AndroidRes(R.string.pump_setting_failed)) } } medtrumPump.pumpStateFlow.collectResilient(scope, aapsLogger, LTag.PUMP) { pumpState -> @@ -310,9 +306,8 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { aapsLogger.error(LTag.PUMPCOMM, "Failed to update pump time") notificationManager.post( NotificationId.PUMP_TIMEZONE_UPDATE_FAILED, - R.string.pump_time_update_failed, - level = NotificationLevel.IMPORTANT, - ) + TextRef.AndroidRes(R.string.pump_time_update_failed), + level = NotificationLevel.IMPORTANT) } } @@ -610,10 +605,9 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { // Show notification to alert user of failure notificationManager.post( NotificationId.PUMP_SYNC_ERROR, - R.string.pump_sync_error, + TextRef.AndroidRes(R.string.pump_sync_error), level = NotificationLevel.URGENT, - sound = AlarmSound.ALARM - ) + sound = AlarmSound.ALARM) } else if (failureCount >= 2) { break } @@ -668,9 +662,8 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { notificationManager.dismiss(NotificationId.PUMP_SUSPENDED) notificationManager.post( NotificationId.PATCH_NOT_ACTIVE, - R.string.patch_not_active, - level = NotificationLevel.IMPORTANT, - ) + TextRef.AndroidRes(R.string.patch_not_active), + level = NotificationLevel.IMPORTANT) medtrumPump.setFakeTBRIfNotSet() medtrumPump.clearAlarmState() @@ -698,10 +691,9 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { notificationManager.post( NotificationId.PUMP_ERROR, - R.string.patch_reset_after_primed_error, + TextRef.AndroidRes(R.string.patch_reset_after_primed_error), level = NotificationLevel.URGENT, - sound = AlarmSound.ALARM - ) + sound = AlarmSound.ALARM) } } @@ -729,8 +721,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { MedtrumPumpState.PAUSED -> { notificationManager.post( NotificationId.PUMP_SUSPENDED, - R.string.pump_is_suspended, - ) + TextRef.AndroidRes(R.string.pump_is_suspended)) // Pump will report proper TBR for this from loadEvents() scope.launch { commandQueue.loadEvents() } } @@ -738,10 +729,9 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { MedtrumPumpState.HOURLY_MAX_SUSPENDED -> { notificationManager.post( NotificationId.PUMP_SUSPENDED, - R.string.pump_is_suspended_hour_max, + TextRef.AndroidRes(R.string.pump_is_suspended_hour_max), level = NotificationLevel.URGENT, - sound = AlarmSound.ALARM - ) + sound = AlarmSound.ALARM) // Pump will report proper TBR for this from loadEvents() scope.launch { commandQueue.loadEvents() } } @@ -749,10 +739,9 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { MedtrumPumpState.DAILY_MAX_SUSPENDED -> { notificationManager.post( NotificationId.PUMP_SUSPENDED, - R.string.pump_is_suspended_day_max, + TextRef.AndroidRes(R.string.pump_is_suspended_day_max), level = NotificationLevel.URGENT, - sound = AlarmSound.ALARM - ) + sound = AlarmSound.ALARM) // Pump will report proper TBR for this from loadEvents() scope.launch { commandQueue.loadEvents() } } @@ -770,9 +759,8 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { // Pump suspended due to error, show error! notificationManager.post( NotificationId.PUMP_ERROR, - R.string.pump_error, alarmState?.let { medtrumPump.alarmStateToString(it) }, - sound = AlarmSound.ALARM - ) + TextRef.AndroidRes(R.string.pump_error, listOf(alarmState?.let { medtrumPump.alarmStateToString(it) }.toString())), + sound = AlarmSound.ALARM) // Get pump status, use readStatus here as for loadEvents() we cannot be sure callback is executed scope.launch { commandQueue.readStatus(rh.gs(app.aaps.core.ui.R.string.device_changed)) @@ -799,9 +787,8 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (medtrumPump.desiredPumpWarning && alarmState != AlarmState.NONE) { notificationManager.post( NotificationId.PUMP_WARNING, - R.string.pump_warning, medtrumPump.alarmStateToString(alarmState), - level = NotificationLevel.ANNOUNCEMENT, - ) + TextRef.AndroidRes(R.string.pump_warning, listOf(medtrumPump.alarmStateToString(alarmState))), + level = NotificationLevel.ANNOUNCEMENT) runBlocking { pumpSync.insertAnnouncement( medtrumPump.alarmStateToString(alarmState), @@ -819,9 +806,8 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { if (dateUtil.now() >= warningAt && dateUtil.now() <= warningAt + CHECK_EXPIRY_WARNING_TIME_MS) { notificationManager.post( NotificationId.PUMP_WARNING, - R.string.alarm_pump_expires_soon, - level = NotificationLevel.ANNOUNCEMENT, - ) + TextRef.AndroidRes(R.string.alarm_pump_expires_soon), + level = NotificationLevel.ANNOUNCEMENT) runBlocking { pumpSync.insertAnnouncement( rh.gs(R.string.alarm_pump_expires_soon), diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 9d465eb92998..7164ae8f9540 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -204,8 +204,7 @@ class OmnipodDashPumpPlugin @Inject constructor( if (!podStateManager.isPodRunning) { notificationManager.post( NotificationId.OMNIPOD_POD_NOT_ATTACHED, - app.aaps.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod - ) + TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod)) } else { notificationManager.dismiss(NotificationId.OMNIPOD_POD_NOT_ATTACHED) if (podStateManager.isSuspended) { @@ -219,9 +218,8 @@ class OmnipodDashPumpPlugin @Inject constructor( if (!podStateManager.sameTimeZone) { notificationManager.post( NotificationId.OMNIPOD_TIME_OUT_OF_SYNC, - R.string.timezone_on_pod_is_different_from_the_timezone, - level = NotificationLevel.NORMAL - ) + TextRef.AndroidRes(R.string.timezone_on_pod_is_different_from_the_timezone), + level = NotificationLevel.NORMAL) } } } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index 82413ea2a4dc..bb0f1a483358 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -9,6 +9,7 @@ import android.os.HandlerThread import android.os.IBinder import android.os.SystemClock import android.text.TextUtils +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType @@ -353,7 +354,7 @@ class OmnipodErosPumpPlugin @Inject constructor( } else { // Not sure what's going on. Notify the user aapsLogger.error(LTag.PUMP, "Unknown TBR in both Pod state and AAPS") - notificationManager.post(NotificationId.OMNIPOD_UNKNOWN_TBR, R.string.omnipod_eros_error_tbr_running_but_aaps_not_aware, sound = AlarmSound.BOLUS_ERROR) + notificationManager.post(NotificationId.OMNIPOD_UNKNOWN_TBR, TextRef.AndroidRes(R.string.omnipod_eros_error_tbr_running_but_aaps_not_aware), sound = AlarmSound.BOLUS_ERROR) } } else if (!podStateManager.isTempBasalRunning && tempBasal != null) { aapsLogger.warn(LTag.PUMP, "Removing AAPS TBR that actually hadn't succeeded") @@ -408,17 +409,17 @@ class OmnipodErosPumpPlugin @Inject constructor( private fun updatePodWarningNotifications() { if (System.currentTimeMillis() > this.nextPodWarningCheck) { if (!podStateManager.isPodRunning) { - notificationManager.post(NotificationId.OMNIPOD_POD_NOT_ATTACHED, app.aaps.pump.omnipod.common.R.string.omnipod_common_error_pod_not_attached) + notificationManager.post(NotificationId.OMNIPOD_POD_NOT_ATTACHED, TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_error_pod_not_attached)) } else { notificationManager.dismiss(NotificationId.OMNIPOD_POD_NOT_ATTACHED) if (podStateManager.isSuspended) { - notificationManager.post(NotificationId.OMNIPOD_POD_SUSPENDED, app.aaps.pump.omnipod.common.R.string.omnipod_common_error_pod_suspended) + notificationManager.post(NotificationId.OMNIPOD_POD_SUSPENDED, TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_error_pod_suspended)) } else { notificationManager.dismiss(NotificationId.OMNIPOD_POD_SUSPENDED) if (podStateManager.timeDeviatesMoreThan(OmnipodConstants.TIME_DEVIATION_THRESHOLD)) { - notificationManager.post(NotificationId.OMNIPOD_TIME_OUT_OF_SYNC, app.aaps.pump.omnipod.common.R.string.omnipod_common_error_time_out_of_sync) + notificationManager.post(NotificationId.OMNIPOD_TIME_OUT_OF_SYNC, TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_error_time_out_of_sync)) } else { notificationManager.dismiss(NotificationId.OMNIPOD_TIME_OUT_OF_SYNC) } @@ -672,9 +673,8 @@ class OmnipodErosPumpPlugin @Inject constructor( notificationManager.post( NotificationId.OMNIPOD_POD_ALERTS_UPDATED, - app.aaps.pump.omnipod.common.R.string.omnipod_common_confirmation_expiration_alerts_updated, - validMinutes = 60 - ) + TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_confirmation_expiration_alerts_updated), + validMinutes = 60) } else { aapsLogger.warn(LTag.PUMP, "Failed to configure alerts in Pod") } @@ -700,9 +700,8 @@ class OmnipodErosPumpPlugin @Inject constructor( if (!requestedByUser && aapsOmnipodErosManager.isTimeChangeEventEnabled) { notificationManager.post( NotificationId.TIME_OR_TIMEZONE_CHANGE, - app.aaps.pump.omnipod.common.R.string.omnipod_common_confirmation_time_on_pod_updated, - validMinutes = 60 - ) + TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_confirmation_time_on_pod_updated), + validMinutes = 60) } } else { if (!requestedByUser) { @@ -712,9 +711,8 @@ class OmnipodErosPumpPlugin @Inject constructor( if (aapsOmnipodErosManager.isTimeChangeEventEnabled) { notificationManager.post( NotificationId.TIME_OR_TIMEZONE_CHANGE, - R.string.omnipod_eros_error_automatic_time_or_timezone_change_failed, - validMinutes = 60 - ) + TextRef.AndroidRes(R.string.omnipod_eros_error_automatic_time_or_timezone_change_failed), + validMinutes = 60) } this.hasTimeDateOrTimeZoneChanged = false timeChangeRetries = 0 @@ -823,7 +821,7 @@ class OmnipodErosPumpPlugin @Inject constructor( } if (!success) { aapsLogger.warn(LTag.PUMP, "Failed to retrieve Pod status on startup") - notificationManager.post(NotificationId.OMNIPOD_STARTUP_STATUS_REFRESH_FAILED, app.aaps.pump.omnipod.common.R.string.omnipod_common_error_failed_to_refresh_status_on_startup) + notificationManager.post(NotificationId.OMNIPOD_STARTUP_STATUS_REFRESH_FAILED, TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_error_failed_to_refresh_status_on_startup)) } } else { aapsLogger.debug(LTag.PUMP, "Not retrieving Pod status on startup: no Pod running") From 5443bf27eaa21b4f634ed7c83883fc9ea64b9412 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 07:13:20 +0200 Subject: [PATCH 105/146] One readableDuration instead of three Omnipod Eros, Omnipod Dash and Equil each carried the same "5 minutes ago" / "2 hours and 10 minutes ago" formatter, identical apart from the resource names. That is where most of the project's plural use lived: 26 rh.gq call sites, 25 of them hours, minutes or days. The logic moves to :core:interfaces once. The strings do not: readableDuration takes a DurationLabels with the caller's own resource ids, so each driver keeps its wording and, more importantly, its translations. Deleting a translated plural family would have shown English to everyone until translators caught up. Equil's copy had no callers at all, so it is simply deleted. Its equil_common_ hours, minutes and days plurals are now unreferenced but left in place, again to avoid throwing translations away. Eros and Dash share the Omnipod wording through OMNIPOD_DURATION_LABELS in :pump:omnipod:common, which both already depend on. rh.gq is down from 26 call sites to 11. What is left is Medtronic (2), plugins/constraints (3), implementation (2), the one non-duration plural (omnipod_common_pod_alerts) and this formatter itself. This also removes the nested gs(gs(gq, gq)) shape from the pump overview view models, which was the main blocker for passing a TextRef as a format argument. The formatter stays on Android on purpose. Plural categories are not just one and other - Czech has one, few, many and other, Arabic has six - so the form has to come from the platform CLDR data via getQuantityString, or a .stringsdict on iOS. Shared code must never pick it. This is now the single place that changes when plurals get a TextRef form. --- .../core/interfaces/utils/ReadableDuration.kt | 77 +++++++++++++++++++ .../equil/compose/EquilOverviewViewModel.kt | 25 ------ .../omnipod/common/OmnipodDurationLabels.kt | 17 ++++ .../dash/ui/compose/DashOverviewViewModel.kt | 31 +------- .../eros/ui/compose/ErosOverviewViewModel.kt | 32 +------- 5 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/ReadableDuration.kt create mode 100644 pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/OmnipodDurationLabels.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/ReadableDuration.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/ReadableDuration.kt new file mode 100644 index 000000000000..9e6109f39e89 --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/utils/ReadableDuration.kt @@ -0,0 +1,77 @@ +package app.aaps.core.interfaces.utils + +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import app.aaps.core.interfaces.resources.ResourceHelper + +/** + * The strings [readableDuration] needs. Each caller passes its own set, so a driver keeps the wording + * and the translations it already has. + * + * @param momentsAgo shown under ten seconds + * @param lessThanAMinuteAgo shown under a minute + * @param timeAgo wraps the amount, for example `%1$s ago` + * @param compositeTime joins two amounts, for example `%1$s and %2$s` + * @param minutes plural for minutes, taking the count as its only argument + * @param hours plural for hours + * @param days plural for days + */ +class DurationLabels( + @StringRes val momentsAgo: Int, + @StringRes val lessThanAMinuteAgo: Int, + @StringRes val timeAgo: Int, + @StringRes val compositeTime: Int, + @PluralsRes val minutes: Int, + @PluralsRes val hours: Int, + @PluralsRes val days: Int +) + +/** + * Formats how long ago something happened, as "5 minutes ago" or "2 hours and 10 minutes ago". + * + * The Omnipod Eros, Omnipod Dash and Equil overviews each carried their own copy of this, identical + * apart from the resource names, which is also where most of the project's plural use sat. It lives + * here once now, and it is the only place that has to change when plurals get a + * [app.aaps.core.keys.interfaces.TextRef] form - a plural cannot be expressed as a TextRef today. + * + * It stays on Android because plural selection must come from the platform: plural categories are not + * just one and other. Czech has one, few, many and other; Arabic has six. `getQuantityString` uses the + * platform CLDR data, and iOS would use a `.stringsdict` the same way. Shared code must never pick the + * form itself. + * + * @param millis how long ago, in milliseconds + */ +fun ResourceHelper.readableDuration(millis: Long, labels: DurationLabels): String { + val seconds = millis / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + + return when { + seconds < 10 -> gs(labels.momentsAgo) + seconds < 60 -> gs(labels.lessThanAMinuteAgo) + seconds < 60 * 60 -> gs(labels.timeAgo, gq(labels.minutes, minutes.toInt(), minutes.toInt())) + + seconds < 24 * 60 * 60 -> { + val minutesLeft = (minutes % 60).toInt() + if (minutesLeft > 0) + gs( + labels.timeAgo, + gs(labels.compositeTime, gq(labels.hours, hours.toInt(), hours.toInt()), gq(labels.minutes, minutesLeft, minutesLeft)) + ) + else + gs(labels.timeAgo, gq(labels.hours, hours.toInt(), hours.toInt())) + } + + else -> { + val days = (hours / 24).toInt() + val hoursLeft = (hours % 24).toInt() + if (hoursLeft > 0) + gs( + labels.timeAgo, + gs(labels.compositeTime, gq(labels.days, days, days), gq(labels.hours, hoursLeft, hoursLeft)) + ) + else + gs(labels.timeAgo, gq(labels.days, days, days)) + } + } +} diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt index 4b4851b2d1f5..5df9b491824b 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt @@ -272,31 +272,6 @@ class EquilOverviewViewModel @Inject constructor( } } - private fun readableDuration(duration: Duration): String { - val hours = duration.toHours().toInt() - val minutes = duration.toMinutes().toInt() - val seconds = duration.seconds - return when { - seconds < 10 -> rh.gs(R.string.equil_common_moments_ago) - seconds < 60 -> rh.gs(R.string.equil_common_less_than_a_minute_ago) - seconds < 60 * 60 -> rh.gs(R.string.equil_common_time_ago, rh.gq(R.plurals.equil_common_minutes, minutes, minutes)) - - seconds < 24 * 60 * 60 -> { - val minutesLeft = minutes % 60 - if (minutesLeft > 0) - rh.gs(R.string.equil_common_time_ago, rh.gs(R.string.equil_common_composite_time, rh.gq(R.plurals.equil_common_hours, hours, hours), rh.gq(R.plurals.equil_common_minutes, minutesLeft, minutesLeft))) - else rh.gs(R.string.equil_common_time_ago, rh.gq(R.plurals.equil_common_hours, hours, hours)) - } - - else -> { - val days = hours / 24 - val hoursLeft = hours % 24 - if (hoursLeft > 0) - rh.gs(R.string.equil_common_time_ago, rh.gs(R.string.equil_common_composite_time, rh.gq(R.plurals.equil_common_days, days, days), rh.gq(R.plurals.equil_common_hours, hoursLeft, hoursLeft))) - else rh.gs(R.string.equil_common_time_ago, rh.gq(R.plurals.equil_common_days, days, days)) - } - } - } // viewModelScope cancels all coroutines automatically on onCleared() } diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/OmnipodDurationLabels.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/OmnipodDurationLabels.kt new file mode 100644 index 000000000000..0bdea76d11b1 --- /dev/null +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/OmnipodDurationLabels.kt @@ -0,0 +1,17 @@ +package app.aaps.pump.omnipod.common + +import app.aaps.core.interfaces.utils.DurationLabels + +/** + * The Omnipod wording for [app.aaps.core.interfaces.utils.readableDuration], shared by Eros and Dash. + * Both drivers showed the same texts through their own copy of the formatter before. + */ +val OMNIPOD_DURATION_LABELS = DurationLabels( + momentsAgo = R.string.omnipod_common_moments_ago, + lessThanAMinuteAgo = R.string.omnipod_common_less_than_a_minute_ago, + timeAgo = R.string.omnipod_common_time_ago, + compositeTime = R.string.omnipod_common_composite_time, + minutes = R.plurals.omnipod_common_minutes, + hours = R.plurals.omnipod_common_hours, + days = R.plurals.omnipod_common_days +) diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt index d2f3d2238eb4..fcf5d939cd45 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt @@ -13,6 +13,8 @@ import androidx.compose.material.icons.filled.Schedule import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.aaps.core.interfaces.utils.readableDuration +import app.aaps.pump.omnipod.common.OMNIPOD_DURATION_LABELS import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger @@ -522,33 +524,8 @@ class DashOverviewViewModel @Inject constructor( return rh.gs(id) } - private fun readableDuration(duration: Duration): String { - val hours = duration.toHours().toInt() - val minutes = duration.toMinutes().toInt() - val seconds = duration.seconds - return when { - seconds < 10 -> rh.gs(CommonR.string.omnipod_common_moments_ago) - seconds < 60 -> rh.gs(CommonR.string.omnipod_common_less_than_a_minute_ago) - seconds < 60 * 60 -> rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_minutes, minutes, minutes)) - - seconds < 24 * 60 * 60 -> { - val minutesLeft = minutes % 60 - if (minutesLeft > 0) - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gs(CommonR.string.omnipod_common_composite_time, rh.gq(CommonR.plurals.omnipod_common_hours, hours, hours), rh.gq(CommonR.plurals.omnipod_common_minutes, minutesLeft, minutesLeft))) - else - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_hours, hours, hours)) - } - - else -> { - val days = hours / 24 - val hoursLeft = hours % 24 - if (hoursLeft > 0) - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gs(CommonR.string.omnipod_common_composite_time, rh.gq(CommonR.plurals.omnipod_common_days, days, days), rh.gq(CommonR.plurals.omnipod_common_hours, hoursLeft, hoursLeft))) - else - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_days, days, days)) - } - } - } + private fun readableDuration(duration: Duration): String = + rh.readableDuration(duration.toMillis(), OMNIPOD_DURATION_LABELS) private fun isQueueEmpty(): Boolean = commandQueue.size() == 0 && commandQueue.performing() == null diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt index 4e7f8c2c4ce1..d11c3bf8af1b 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt @@ -15,6 +15,8 @@ import androidx.compose.material.icons.filled.Schedule import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.aaps.core.interfaces.utils.readableDuration +import app.aaps.pump.omnipod.common.OMNIPOD_DURATION_LABELS import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger @@ -539,34 +541,8 @@ class ErosOverviewViewModel @Inject constructor( return rh.gs(CommonR.string.omnipod_common_time_with_timezone, dateUtil.dateAndTimeString(timeAsJavaDate.time), tzDisplayName) } - private fun readableDuration(dateTime: DateTime): String { - val duration = Duration(dateTime, DateTime.now()) - val hours = duration.standardHours.toInt() - val minutes = duration.standardMinutes.toInt() - val seconds = duration.standardSeconds.toInt() - return when { - seconds < 10 -> rh.gs(CommonR.string.omnipod_common_moments_ago) - seconds < 60 -> rh.gs(CommonR.string.omnipod_common_less_than_a_minute_ago) - seconds < 60 * 60 -> rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_minutes, minutes, minutes)) - - seconds < 24 * 60 * 60 -> { - val minutesLeft = minutes % 60 - if (minutesLeft > 0) - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gs(CommonR.string.omnipod_common_composite_time, rh.gq(CommonR.plurals.omnipod_common_hours, hours, hours), rh.gq(CommonR.plurals.omnipod_common_minutes, minutesLeft, minutesLeft))) - else - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_hours, hours, hours)) - } - - else -> { - val days = hours / 24 - val hoursLeft = hours % 24 - if (hoursLeft > 0) - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gs(CommonR.string.omnipod_common_composite_time, rh.gq(CommonR.plurals.omnipod_common_days, days, days), rh.gq(CommonR.plurals.omnipod_common_hours, hoursLeft, hoursLeft))) - else - rh.gs(CommonR.string.omnipod_common_time_ago, rh.gq(CommonR.plurals.omnipod_common_days, days, days)) - } - } - } + private fun readableDuration(dateTime: DateTime): String = + rh.readableDuration(DateTime.now().millis - dateTime.millis, OMNIPOD_DURATION_LABELS) private fun isQueueEmpty(): Boolean = commandQueue.size() == 0 && commandQueue.performing() == null From 57997e348e517962ea0f38b66a50302e0fbe052e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 07:16:47 +0200 Subject: [PATCH 106/146] Delete the Equil duration strings, dead with their formatter Equil's readableDuration had no callers and went in the previous commit. Its seven resources went dead with it: the hours, minutes and days plurals plus moments_ago, less_than_a_minute_ago, time_ago and composite_time. Nothing references them anywhere. Removed from all 17 locale files, not only English. A translation whose base string is gone is not something to leave behind. I kept them last time to avoid discarding translator work, but they are dead, and dead is dead - if Equil ever needs this wording again it uses the shared formatter, which has the Omnipod labels or new ones. --- .../src/main/res/values-bg-rBG/strings.xml | 16 -------------- .../src/main/res/values-cs-rCZ/strings.xml | 22 ------------------- .../src/main/res/values-es-rES/strings.xml | 16 -------------- .../src/main/res/values-fr-rFR/strings.xml | 16 -------------- .../src/main/res/values-it-rIT/strings.xml | 16 -------------- .../src/main/res/values-lt-rLT/strings.xml | 22 ------------------- .../src/main/res/values-nb-rNO/strings.xml | 16 -------------- .../src/main/res/values-nl-rNL/strings.xml | 16 -------------- .../src/main/res/values-pl-rPL/strings.xml | 22 ------------------- .../src/main/res/values-ro-rRO/strings.xml | 19 ---------------- .../src/main/res/values-ru-rRU/strings.xml | 22 ------------------- .../src/main/res/values-sk-rSK/strings.xml | 22 ------------------- .../src/main/res/values-tr-rTR/strings.xml | 16 -------------- .../src/main/res/values-vi-rVN/strings.xml | 13 ----------- .../src/main/res/values-zh-rCN/strings.xml | 13 ----------- .../src/main/res/values-zh-rTW/strings.xml | 13 ----------- pump/equil/src/main/res/values/strings.xml | 16 -------------- 17 files changed, 296 deletions(-) diff --git a/pump/equil/src/main/res/values-bg-rBG/strings.xml b/pump/equil/src/main/res/values-bg-rBG/strings.xml index 2769cab913fd..66e8bed67d58 100644 --- a/pump/equil/src/main/res/values-bg-rBG/strings.xml +++ b/pump/equil/src/main/res/values-bg-rBG/strings.xml @@ -114,24 +114,8 @@ Неправилна инфузия е спряна Спиране Възобновяване доставянето на инсулин - Преди малко - Преди по-малко от минута - преди %1$sсек - %1$s и %2$s Не сте завършили стъпките. Сигурни ли сте? Изход - - %1$d минутa - %1$d минутa - - - %1$d час - %1$d часа - - - %1$d дeн - %1$d days - Версията на фърмуера е твърде ниска и не се поддържа Парола за връзка с Equil v5.3 помпа. Tя ще се използва и за разкачане, ЗАПОМНЕТЕ Я. Праволото е: 4 символа, между ABCDEF0123456789. Парола diff --git a/pump/equil/src/main/res/values-cs-rCZ/strings.xml b/pump/equil/src/main/res/values-cs-rCZ/strings.xml index a42eabb0cfda..badfb5f9b709 100644 --- a/pump/equil/src/main/res/values-cs-rCZ/strings.xml +++ b/pump/equil/src/main/res/values-cs-rCZ/strings.xml @@ -114,30 +114,8 @@ Zastaveno abnormální podávání izulínu Pozastavit Obnovit podávání inzulínu - Před chvílí - Před méně než minutou - před %1$s - %1$s a %2$s Dosud jste nedokončili všechny kroky. Jste si jisti, že chcete skončit? Odejít - - %1$d minuta - %1$d minut - %1$d minut - %1$d minut - - - %1$d hodina - %1$d hodin - %1$d hodin - %1$d hodin - - - %1$d den - %1$d dní - %1$d dní - %1$d dní - Verze firmwaru je příliš stará. Není podporována. Volitelně nastavte heslo pro spárování s pumpou Equil v5.3. Toto heslo bude použito i pro budoucí odpárování, proto si jej dobře zapamatujte. Pravidlo hesla: musí být 4 znaky dlouhé, z množiny ABCDEF0123456789. Nastavit heslo pro párování diff --git a/pump/equil/src/main/res/values-es-rES/strings.xml b/pump/equil/src/main/res/values-es-rES/strings.xml index 5b43f1fffeaa..6c0eb0a2138e 100644 --- a/pump/equil/src/main/res/values-es-rES/strings.xml +++ b/pump/equil/src/main/res/values-es-rES/strings.xml @@ -114,24 +114,8 @@ Infusión anormal detenida Suspender Reanudar la entrega - Hace un momento - Hace menos de un minuto - %1$s ago - %1$s and %2$s Aún no has completado todos los pasos. ¿Estás seguro de que quieres salir? Salir - - %1$d minuto - %1$d minutos - - - %1$d hora - %1$d horas - - - %1$d día - %1$d días - Versión del firmware demasiado baja. No compatible Opcionalmente, establezca una contraseña para el emparejamiento con la bomba Equil v5.3. Esta contraseña también se usará para desvincularla en el futuro, así que asegúrese de recordarla. Regla de la contraseña: Debe tener 4 caracteres, elegidos entre ABCDEF0123456789. Establecer la contraseña de emparejamiento diff --git a/pump/equil/src/main/res/values-fr-rFR/strings.xml b/pump/equil/src/main/res/values-fr-rFR/strings.xml index f971f39730b0..253753491962 100644 --- a/pump/equil/src/main/res/values-fr-rFR/strings.xml +++ b/pump/equil/src/main/res/values-fr-rFR/strings.xml @@ -114,24 +114,8 @@ Injection anormale arrêtée Suspendre Reprendre l\'injection - À l\'instant - Il y a moins d\'une minute - il y a %1$s - %1$s et %2$s Vous n\'avez pas encore terminé toutes les étapes. Êtes-vous sûr de vouloir quitter ? Quitter - - %1$d minute - %1$d minutes - - - %1$d heure - %1$d heures - - - %1$d jour - %1$d jours - Version du firmware trop ancienne. Non pris en charge Optionnellement, définisez un mot de passe pour l\'appairage avec l\'Equil v5.3. Ce mot de passe sera également utilisé pour dissocier la pompe actuelle dans le futur, donc n\'oubliez pas de le noter. Règle pour le mot de passe : Il doit comporter 4 caractères, choisi parmi ABCDEF0123456789. Définir le mot de passe d\'appairage diff --git a/pump/equil/src/main/res/values-it-rIT/strings.xml b/pump/equil/src/main/res/values-it-rIT/strings.xml index 20f06acf1ace..3a41f76c31f5 100644 --- a/pump/equil/src/main/res/values-it-rIT/strings.xml +++ b/pump/equil/src/main/res/values-it-rIT/strings.xml @@ -114,24 +114,8 @@ Infusione Anomala Fermata Sospendi Riprendi erogazione - Un momento fa - Meno di 1 minuto fa - %1$sm fa - %1$s e %2$s Non hai ancora completato tutti i passaggi. Sei sicuro di voler uscire? Esci - - %1$d minuto - %1$d minuti - - - %1$d ora - %1$d ore - - - %1$d giorno - %1$d giorni - Versione firmware troppo vecchia. Non supportata Si prega di impostare una nuova password per l\'accoppiamento col micro Equil v5.3. Questa password verrà utilizzata anche per disaccoppiarsi con la pompa in futuro, quindi assicurati di ricordarla. Regola password: deve contenere 4 caratteri, scelti tra ABCDEF0123456789. diff --git a/pump/equil/src/main/res/values-lt-rLT/strings.xml b/pump/equil/src/main/res/values-lt-rLT/strings.xml index c15c3c2cbfeb..25657b446b1b 100644 --- a/pump/equil/src/main/res/values-lt-rLT/strings.xml +++ b/pump/equil/src/main/res/values-lt-rLT/strings.xml @@ -104,30 +104,8 @@ Neįprastas suleidimas sustabdytas Sustabdyti Atnaujinti suleidimą - Neseniai - Mažiau nei prieš minutę - prieš %1$s - %1$s ir %2$s Dar nesate užbaigę visų žingsnių. Ar tikrai norite išeiti? Išeiti - - %1$d minutė - %1$d minutės - %1$d minutės - %1$d min. - - - %1$d valanda - %1$d valandos - %1$d valandų - %1$d val. - - - %1$d diena - %1$d dienos - %1$d d. - %1$d d. - Programinės įrangos versija per žema. Nepalaikoma Nustatyti suporavimo slaptažodį Po to, kai uždėsite pompą ant pagrindo plokštės, pasirinkite, ar norite išstumti orą iš įvestos adatos. diff --git a/pump/equil/src/main/res/values-nb-rNO/strings.xml b/pump/equil/src/main/res/values-nb-rNO/strings.xml index eaa018bcd9c5..253cc8b33ef0 100644 --- a/pump/equil/src/main/res/values-nb-rNO/strings.xml +++ b/pump/equil/src/main/res/values-nb-rNO/strings.xml @@ -114,24 +114,8 @@ Unormal infusjon stoppet Pause Gjenoppta tilførselen - For litt siden - Mindre enn ett minutt siden - %1$s siden - %1$s og %2$s Du har ikke fullført alle trinn ennå. Er du sikker på at du vil avslutte? Avslutt - - %1$d minutt - %1$d minutter - - - %1$d time - %1$d timer - - - %1$d dag - %1$d dager - Firmwareversjonen er for lav. Støttes ikke Angi eventuelt et passord for paring med Equil v5.3-pumpen. Dette passordet vil også bli brukt til å oppheve paringen i fremtiden, så husk det. Passordregel: Den må være på 4 tegn, valgt fra ABCDEF0123456789. Sett sammenkoblingspassord diff --git a/pump/equil/src/main/res/values-nl-rNL/strings.xml b/pump/equil/src/main/res/values-nl-rNL/strings.xml index 87a5a7eb9582..3e870d04c203 100644 --- a/pump/equil/src/main/res/values-nl-rNL/strings.xml +++ b/pump/equil/src/main/res/values-nl-rNL/strings.xml @@ -104,24 +104,8 @@ Abnormale toediening gestopt Onderbreken Insulinetoediening hervatten - Zojuist - Minder dan een minuut geleden - %1$s geleden - %1$s en %2$s Je hebt nog niet alle stappen voltooid. Weet je zeker dat je wilt afsluiten? Afsluiten - - %1$d minuut - %1$d minuten - - - %1$d uur - %1$d uren - - - %1$d dag - %1$d dagen - Firmware versie te laag. Niet ondersteund Instellen koppelwachtwoord Na het installeren van de pomp op het basisplaat, selecteer of de lucht uit de binnenste naald moet worden verwijderd. diff --git a/pump/equil/src/main/res/values-pl-rPL/strings.xml b/pump/equil/src/main/res/values-pl-rPL/strings.xml index f1e5432e75d1..c0103c8c86c7 100644 --- a/pump/equil/src/main/res/values-pl-rPL/strings.xml +++ b/pump/equil/src/main/res/values-pl-rPL/strings.xml @@ -104,30 +104,8 @@ Nieprawidłowa infuzja zatrzymana Zawieś Wznów podawanie - Chwilę temu - Mniej niż minutę temu - %1$s temu - %1$s i %2$s Nie ukończono jeszcze wszystkich kroków. Czy na pewno chcesz zakończyć? Wyjście - - %1$d minuta - %1$d minuty - %1$d minut - %1$d minut - - - %1$d godzina - %1$d godziny - %1$d godzin - %1$d godzin - - - %1$d dzień - %1$d dni - %1$d dni - %1$d dni - Zbyt niska wersja firmware\'u urządzenia - nie jest obsługiwana Ustaw hasło parowania Po zainstalowaniu pompy na płytce bazowej wybierz, czy usunąć powietrze z igły, która ma być wkłuta. diff --git a/pump/equil/src/main/res/values-ro-rRO/strings.xml b/pump/equil/src/main/res/values-ro-rRO/strings.xml index 2361632d1781..e52dbf9d8ac6 100644 --- a/pump/equil/src/main/res/values-ro-rRO/strings.xml +++ b/pump/equil/src/main/res/values-ro-rRO/strings.xml @@ -115,27 +115,8 @@ Infuzie anormală oprită Suspendați Reluați administrarea - Momente în urmă - Mai puțin de un minut în urmă - %1$s în urmă - %1$s și %2$s Nu ați finalizat încă toți pașii. Sigur vreți să ieșiți? Ieșire - - %1$d minut - %1$d minute - %1$d minute - - - %1$d oră - %1$d ore - %1$d ore - - - %1$d zi - %1$d zile - %1$d zile - Versiunea firmware este prea veche. Nu este acceptată Opțional setați o parolă pentru asocierea cu pompa Equil v5.3. Această parolă va fi folosită și pentru dezasocierea în viitor, așa că vă rugăm să vă amintiți de ea. Regulă parolă: trebuie să aibă 4 caractere, aleasă din ABCDEF0123456789. Setați parolă asociere diff --git a/pump/equil/src/main/res/values-ru-rRU/strings.xml b/pump/equil/src/main/res/values-ru-rRU/strings.xml index f94a4dede622..5b9d97b2a946 100644 --- a/pump/equil/src/main/res/values-ru-rRU/strings.xml +++ b/pump/equil/src/main/res/values-ru-rRU/strings.xml @@ -104,30 +104,8 @@ Аномалия инфузии остановлена Останов Возобновить ввод - Только что - Менее минуты назад - %1$s назад - %1$s и %2$s Еще Не выполнены все шаги. Действительно хотите выйти? Выйти - - %1$d минута - %1$d минуты - %1$d минут - %1$d мин - - - %1$d час - %1$d часа - %1$d часов - %1$d час - - - %1$d день - %1$d дня - %1$d дней - %1$d дн - Версия прошивки слишком мала. Не поддерживается Установите пароль сопряжения После установки помпы на платформу выберите, следует ли выдуть воздух из иглы. diff --git a/pump/equil/src/main/res/values-sk-rSK/strings.xml b/pump/equil/src/main/res/values-sk-rSK/strings.xml index affb93fa44c6..39642e0cec6e 100644 --- a/pump/equil/src/main/res/values-sk-rSK/strings.xml +++ b/pump/equil/src/main/res/values-sk-rSK/strings.xml @@ -114,30 +114,8 @@ Zastavené abnormálne podávanie izulínu Pozastaviť Obnoviť podávanie inzulínu - Pred chvíľou - Pred menej ako minútou - pred %1$s - %1$s a %2$s Doposiaľ ste nedokončili všetky kroky. Ste si istý, že chcete ukončiť prácu? Ukončiť - - %1$d minúta - %1$d minút - %1$d minút - %1$d minút - - - %1$d hodina - %1$d hodín - %1$d hodín - %1$d hodín - - - %1$d deň - %1$d dní - %1$d dní - %1$d dní - Verzia firmvéru je príliš stará. Nie je podporovaná. Ak chcete spárovať pumpu Equil v5.3, nastavte heslo. Toto heslo bude v budúcnosti použité aj pre zrušenie párovania, preto si ho zapamätajte. Pravidlo pre tvorbu hesla: musí mať 4 znaky a musí obsahovať znaky z tejto skupiny ABCDEF0123456789. diff --git a/pump/equil/src/main/res/values-tr-rTR/strings.xml b/pump/equil/src/main/res/values-tr-rTR/strings.xml index ccfa5acd1d57..f6f3efc45e59 100644 --- a/pump/equil/src/main/res/values-tr-rTR/strings.xml +++ b/pump/equil/src/main/res/values-tr-rTR/strings.xml @@ -104,24 +104,8 @@ Anormal İnfüzyon Durduruldu Askıya al İletime devam et - Dakika önce - Bir dakikadan az önce - %1$s dak önce - %1$s ve %2$s Henüz tüm adımları tamamlamadınız. Çıkmak istediğinize emin misin? Çıkış - - %1$d dakika - %1$d dakika - - - %1$d saat - %1$d saat - - - %1$d gün - %1$d gün - Cihaz yazılımı sürümü çok düşük. Desteklenmiyor Paylaşım şifresini belirleyin Pompayı tabana yerleştirdikten sonra, havanın kalıcı iğneden boşaltılıp boşaltılmayacağını seçin. diff --git a/pump/equil/src/main/res/values-vi-rVN/strings.xml b/pump/equil/src/main/res/values-vi-rVN/strings.xml index 9ce1078a4259..e9559d717399 100644 --- a/pump/equil/src/main/res/values-vi-rVN/strings.xml +++ b/pump/equil/src/main/res/values-vi-rVN/strings.xml @@ -114,21 +114,8 @@ Đã dừng lại do dây truyền bất thường Tạm dừng Tiếp tục bơm insulin - Cách đây ít phút - Chưa đầy một phút trước - %1$s trước - %1$s và %2$s Bạn chưa hoàn tất tất cả các bước. Bạn có chắc chắn muốn thoát không? Thoát - - %1$d phút - - - %1$d giờ - - - %1$d ngày - Phiên bản firmware quá thấp. Không được hỗ trợ Vui lòng đặt mật khẩu mới để ghép nối với bơm Equil v5.3. Mật khẩu này cũng sẽ được sử dụng để hủy ghép nối với bơm hiện tại trong dự kiến, vì vậy hãy chắc chắn ghi nhớ. Quy tắc mật khẩu: gồm 4 ký tự, chọn từ ABCDEF0123456789. Đặt mật khẩu ghép nối diff --git a/pump/equil/src/main/res/values-zh-rCN/strings.xml b/pump/equil/src/main/res/values-zh-rCN/strings.xml index a572617fcce0..cbac8cb09d1c 100644 --- a/pump/equil/src/main/res/values-zh-rCN/strings.xml +++ b/pump/equil/src/main/res/values-zh-rCN/strings.xml @@ -114,21 +114,8 @@ 异常输注停止 暂停 恢复输注 - 刚刚 - 不到一分钟前 - %1$s 前 - %1$s 和 %2$s 您尚未完成所有步骤。您确定要退出吗? 退出 - - %1$d 分钟 - - - %1$d 小时 - - - %1$d 天 - 固件版本过低。不支持 可选设置与Equil v5.3 泵配对的密码。 此密码将来也会用于取消配对,所以请务必记住它。 密码规则:长度必须为4个字符,从ABCDEF0123456789中选择。 设置配对密码 diff --git a/pump/equil/src/main/res/values-zh-rTW/strings.xml b/pump/equil/src/main/res/values-zh-rTW/strings.xml index 0246d9e7a0d8..b3d91ad48233 100644 --- a/pump/equil/src/main/res/values-zh-rTW/strings.xml +++ b/pump/equil/src/main/res/values-zh-rTW/strings.xml @@ -114,21 +114,8 @@ 異常輸液停止 暫停 恢復輸送 - 片刻前 - 不到一分鐘前 - %1$s前 - %1$s和%2$s 您尚未完成所有步驟。您確定要退出嗎? 退出 - - %1$d 分鐘 - - - %1$d 小時 - - - %1$d 天 - 韌體版本過低。不支援 可選擇為與 Equil v5.3 幫浦配對設定密碼。此密碼未來也將用於取消配對,請務必記住。密碼規則:必須為 4 個字元,且只能從 ABCDEF0123456789 中選擇。 設定配對密碼 diff --git a/pump/equil/src/main/res/values/strings.xml b/pump/equil/src/main/res/values/strings.xml index 7c2c0eb68976..d3aed6f34180 100644 --- a/pump/equil/src/main/res/values/strings.xml +++ b/pump/equil/src/main/res/values/strings.xml @@ -132,24 +132,8 @@ Suspend Resume delivery - Moments ago - Less than a minute ago - %1$s ago - %1$s and %2$s You haven\'t completed all steps yet. Are you sure you want to exit? Exit - - %1$d minute - %1$d minutes - - - %1$d hour - %1$d hours - - - %1$d day - %1$d days - Firmware version too low. Not supported Optionally set a password for pairing with the Equil v5.3 pump. This password will also be used for unpairing in the future, so please be sure to remember it. Password rule: It must be 4 characters long, chosen from ABCDEF0123456789. Set pair password From 978f0ef23430596de6715f781b92609de55ecfd7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 07:39:26 +0200 Subject: [PATCH 107/146] Events that carry data are data classes, and Event moves to commonMain Event.toString() used commons-lang3 ReflectionToStringBuilder, which was the only thing keeping the base class on Android. The 23 events that carry constructor parameters become data classes, so they print their own values. The 41 that carry none keep the base toString, which is now just the class name - the same thing reflection produced for a class with no fields. commons-lang3 is gone from :core:interfaces. Nothing depended on event identity, so the change from identity to content equality is safe: no distinctUntilChanged on a bus stream, no event in a Set or Map, no event compared with ==. The dedup key documented on EventShowSnackbar is not actually implemented in the host, so it does not rely on equality either. None of the 23 was open or subclassed, so all could take data. EventMobileToWearWatchface holds a ByteArray, so its generated toString prints the reference rather than the bytes reflection used to dump. For a watchface payload that is an improvement. RxBus is now one step from commonMain: Class in toFlow is all that is left. --- .../aaps/core/interfaces/rx/events/Event.kt | 18 ------------------ .../events/EventAutosensCalculationFinished.kt | 2 +- .../core/interfaces/rx/events/EventBTChange.kt | 2 +- .../interfaces/rx/events/EventMobileToWear.kt | 2 +- .../rx/events/EventMobileToWearWatchface.kt | 2 +- .../interfaces/rx/events/EventNtpStatus.kt | 2 +- .../rx/events/EventProfileChangeRequested.kt | 2 +- .../rx/events/EventRefreshButtonState.kt | 2 +- .../rx/events/EventRefreshOverview.kt | 2 +- .../interfaces/rx/events/EventSWRLStatus.kt | 2 +- .../interfaces/rx/events/EventSWSyncStatus.kt | 2 +- .../core/interfaces/rx/events/EventSWUpdate.kt | 2 +- .../interfaces/rx/events/EventShowSnackbar.kt | 2 +- .../events/EventUpdateOverviewCalcProgress.kt | 2 +- .../rx/events/EventWearDataToMobile.kt | 2 +- .../interfaces/rx/events/EventWearToMobile.kt | 2 +- .../interfaces/rx/events/EventWearUpdateGui.kt | 2 +- .../aaps/core/interfaces/rx/events/Event.kt | 13 +++++++++++++ .../plugins/aps/events/EventResetOpenAPSGui.kt | 2 +- .../aps/loop/events/EventLoopSetLastRunGui.kt | 2 +- .../automation/events/EventLocationChange.kt | 2 +- .../tidepool/events/EventTidepoolStatus.kt | 2 +- .../pump/dana/events/EventDanaRSyncStatus.kt | 2 +- .../aaps/pump/eopatch/event/EoPatchEvents.kt | 2 +- .../aaps/pump/equil/events/EventEquilAlarm.kt | 2 +- 25 files changed, 36 insertions(+), 41 deletions(-) delete mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt create mode 100644 core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt deleted file mode 100644 index e1a7edefb727..000000000000 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.aaps.core.interfaces.rx.events - -import org.apache.commons.lang3.builder.ReflectionToStringBuilder -import org.apache.commons.lang3.builder.ToStringStyle - -/** Base class for all events posted on the event bus. */ -abstract class Event { - - override fun toString(): String { - return ReflectionToStringBuilder.toString(this) - } - - companion object { - init { - ReflectionToStringBuilder.setDefaultStyle(ToStringStyle.SHORT_PREFIX_STYLE) - } - } -} \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt index 09aabdd6cf99..c552b469eb6d 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt @@ -1,3 +1,3 @@ package app.aaps.core.interfaces.rx.events -class EventAutosensCalculationFinished(val triggeredByNewBG: Boolean) : EventLoop() +data class EventAutosensCalculationFinished(val triggeredByNewBG: Boolean) : EventLoop() diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt index 306825e88e76..e9f2e1d00a91 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt @@ -7,7 +7,7 @@ package app.aaps.core.interfaces.rx.events * @param deviceName The name of the device, or null if not available. * @param deviceAddress The address of the device, or null if not available. */ -class EventBTChange(val state: Change, val deviceName: String?, @Suppress("unused") val deviceAddress: String? = null) : Event() { +data class EventBTChange(val state: Change, val deviceName: String?, @Suppress("unused") val deviceAddress: String? = null) : Event() { /** * Represents the connection state of a Bluetooth device. diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt index be731216cc28..dfff12517ec1 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt @@ -8,4 +8,4 @@ import app.aaps.core.interfaces.rx.weardata.EventData * * @param payload The data to send. */ -class EventMobileToWear(val payload: EventData) : Event() \ No newline at end of file +data class EventMobileToWear(val payload: EventData) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt index 2fcac3e2338c..9d3c62bbddaf 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt @@ -1,3 +1,3 @@ package app.aaps.core.interfaces.rx.events -class EventMobileToWearWatchface(val payload: ByteArray) : Event() \ No newline at end of file +data class EventMobileToWearWatchface(val payload: ByteArray) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt index cf7c61ae8797..147251de11f6 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt @@ -6,4 +6,4 @@ package app.aaps.core.interfaces.rx.events * @param status The status message. * @param percent The progress percentage. */ -class EventNtpStatus(val status: String, val percent: Int) : Event() \ No newline at end of file +data class EventNtpStatus(val status: String, val percent: Int) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt index 16056ce81816..f52d6a5fca46 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt @@ -9,4 +9,4 @@ package app.aaps.core.interfaces.rx.events * own ProfileSwitch on end — an internal, automatic write the user shouldn't have to acknowledge * (issue #4959). Defaults to false so every existing sender keeps its current behavior. */ -class EventProfileChangeRequested(val silent: Boolean = false) : Event() +data class EventProfileChangeRequested(val silent: Boolean = false) : Event() diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt index aa59de9db86c..74f492cac98e 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt @@ -5,4 +5,4 @@ package app.aaps.core.interfaces.rx.events * * @param newState The new state of the button (e.g., enabled/disabled). */ -class EventRefreshButtonState(val newState: Boolean) : Event() \ No newline at end of file +data class EventRefreshButtonState(val newState: Boolean) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt index 40715eba2665..a54057916198 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt @@ -6,4 +6,4 @@ package app.aaps.core.interfaces.rx.events * @param from A string indicating the source of the refresh request. * @param now If true, the refresh should happen immediately. */ -class EventRefreshOverview(var from: String, val now: Boolean = false) : Event() \ No newline at end of file +data class EventRefreshOverview(var from: String, val now: Boolean = false) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt index 36f2ae63fdfc..4a5357dddc71 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt @@ -7,7 +7,7 @@ import app.aaps.core.keys.interfaces.TextRef * * @param status The RileyLink status message. */ -class EventSWRLStatus(val status: String) : EventStatus() { +data class EventSWRLStatus(val status: String) : EventStatus() { override fun getStatus(): TextRef = TextRef.Literal(status) } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt index 20a3b8d85162..c9cf253f7fef 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt @@ -7,7 +7,7 @@ import app.aaps.core.keys.interfaces.TextRef * * @param status The sync status message. */ -class EventSWSyncStatus(val status: String) : EventStatus() { +data class EventSWSyncStatus(val status: String) : EventStatus() { override fun getStatus(): TextRef = TextRef.Literal(status) } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt index 5a9999affbfb..eb96d4051a0c 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt @@ -5,4 +5,4 @@ package app.aaps.core.interfaces.rx.events * * @param redraw If true, forces a redraw of the wizard. */ -class EventSWUpdate(var redraw: Boolean) : Event() \ No newline at end of file +data class EventSWUpdate(var redraw: Boolean) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt index 3ac39ec9a7df..df968aa80820 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt @@ -13,7 +13,7 @@ package app.aaps.core.interfaces.rx.events * @param key Optional dedup key. If set, rapid duplicates with the same * key collapse so retry loops don't flood the host. */ -class EventShowSnackbar( +data class EventShowSnackbar( val message: String, val type: Type = Type.Info, val key: String? = null diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt index 9f54da091559..1cf49b4af2f8 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt @@ -1,3 +1,3 @@ package app.aaps.core.interfaces.rx.events -class EventUpdateOverviewCalcProgress(val from: String) : Event() \ No newline at end of file +data class EventUpdateOverviewCalcProgress(val from: String) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt index 02415db3507f..44977b5343d7 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt @@ -8,4 +8,4 @@ import app.aaps.core.interfaces.rx.weardata.EventData * * @param payload The data to send. */ -class EventWearDataToMobile(val payload: EventData) : Event() \ No newline at end of file +data class EventWearDataToMobile(val payload: EventData) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt index 7fb2628361af..479e6ef551e6 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt @@ -7,4 +7,4 @@ import app.aaps.core.interfaces.rx.weardata.EventData * * @param payload The data to send. */ -class EventWearToMobile(val payload: EventData) : Event() \ No newline at end of file +data class EventWearToMobile(val payload: EventData) : Event() \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt index 8428dbe15466..d2fe195a9952 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt @@ -8,4 +8,4 @@ import app.aaps.core.interfaces.rx.weardata.CwfData * @param customWatchfaceData Data for a custom watchface, or null if not applicable. * @param exportFile If true, the data should be exported to a file. */ -class EventWearUpdateGui(val customWatchfaceData: CwfData? = null, val exportFile: Boolean = false) : Event() \ No newline at end of file +data class EventWearUpdateGui(val customWatchfaceData: CwfData? = null, val exportFile: Boolean = false) : Event() \ No newline at end of file diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt new file mode 100644 index 000000000000..08f5e57861e0 --- /dev/null +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/Event.kt @@ -0,0 +1,13 @@ +package app.aaps.core.interfaces.rx.events + +/** + * Base class for all events posted on the event bus. + * + * Events that carry data are `data class`es, so they print their values themselves. This default is + * for the ones that carry none: for those the name is the whole message, and it is what the previous + * reflection based toString produced for them anyway. + */ +abstract class Event { + + override fun toString(): String = this::class.simpleName ?: "Event" +} diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/events/EventResetOpenAPSGui.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/events/EventResetOpenAPSGui.kt index d8e53f4b6ae6..91dc7c3b8aef 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/events/EventResetOpenAPSGui.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/events/EventResetOpenAPSGui.kt @@ -2,4 +2,4 @@ package app.aaps.plugins.aps.events import app.aaps.core.interfaces.rx.events.EventUpdateGui -class EventResetOpenAPSGui(val text: String) : EventUpdateGui() +data class EventResetOpenAPSGui(val text: String) : EventUpdateGui() diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/events/EventLoopSetLastRunGui.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/events/EventLoopSetLastRunGui.kt index 6b9ad3395e8b..58105118fc4f 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/events/EventLoopSetLastRunGui.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/events/EventLoopSetLastRunGui.kt @@ -5,4 +5,4 @@ import app.aaps.core.interfaces.rx.events.EventUpdateGui /** * Created by mike on 05.08.2016. */ -class EventLoopSetLastRunGui(val text: String) : EventUpdateGui() +data class EventLoopSetLastRunGui(val text: String) : EventUpdateGui() diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/events/EventLocationChange.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/events/EventLocationChange.kt index 26be7ca74f07..a5eab935fd77 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/events/EventLocationChange.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/events/EventLocationChange.kt @@ -3,4 +3,4 @@ package app.aaps.plugins.automation.events import android.location.Location import app.aaps.core.interfaces.rx.events.Event -class EventLocationChange(var location: Location) : Event() +data class EventLocationChange(var location: Location) : Event() diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt index 672c0104693a..5e12cdae33af 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt @@ -4,7 +4,7 @@ import app.aaps.core.interfaces.rx.events.Event import java.text.SimpleDateFormat import java.util.Locale -class EventTidepoolStatus(val status: String) : Event() { +data class EventTidepoolStatus(val status: String) : Event() { var date: Long = System.currentTimeMillis() diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/events/EventDanaRSyncStatus.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/events/EventDanaRSyncStatus.kt index f7fbb9a2366e..1acfc2fe0471 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/events/EventDanaRSyncStatus.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/events/EventDanaRSyncStatus.kt @@ -7,4 +7,4 @@ import app.aaps.core.interfaces.rx.events.Event * * @param message The sync status message. */ -class EventDanaRSyncStatus(var message: String) : Event() \ No newline at end of file +data class EventDanaRSyncStatus(var message: String) : Event() \ No newline at end of file diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/event/EoPatchEvents.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/event/EoPatchEvents.kt index 1bbb159095ea..cb9fb6e01c64 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/event/EoPatchEvents.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/event/EoPatchEvents.kt @@ -3,5 +3,5 @@ package app.aaps.pump.eopatch.event import app.aaps.core.interfaces.rx.events.Event import app.aaps.pump.eopatch.alarm.AlarmCode -class EventEoPatchAlarm(val alarmCodes: Set, val isFirst: Boolean = false) : Event() +data class EventEoPatchAlarm(val alarmCodes: Set, val isFirst: Boolean = false) : Event() class EventPatchActivationNotComplete : Event() diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/events/EventEquilAlarm.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/events/EventEquilAlarm.kt index db385c16fa22..f99d4b69b0a0 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/events/EventEquilAlarm.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/events/EventEquilAlarm.kt @@ -2,4 +2,4 @@ package app.aaps.pump.equil.events import app.aaps.core.interfaces.rx.events.Event -class EventEquilAlarm(var tips: String) : Event() \ No newline at end of file +data class EventEquilAlarm(var tips: String) : Event() \ No newline at end of file From 19d23293cfd91819f0197e915485210e1ca96641 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 07:53:44 +0200 Subject: [PATCH 108/146] RxBus keys on KClass, and takes 48 files to commonMain with it Class in toFlow was the last thing keeping RxBus on Android. KClass is multiplatform and isInstance works the same, so the signature becomes toFlow(eventType: KClass) and 208 call sites drop one token: ::class.java becomes ::class. Three places pass the type in a variable rather than a literal and change with it: RxHelper in androidTest, SWEventListener, and the two test fakes of RxBus. RxHelper's maps and its waitFor and resetState take KClass now too, so the file is consistent, and its callers plus SWDefinition follow. With that and Event moving in the previous commit, the fixpoint jumps from 156 files in commonMain to 204, leaving 56. Six rounds again: 31 files fail on their own imports, then 11, 10, 3 and 1. Sensitivity, IobCobCalculator, AutosensDataStore and the rest of that chain came across. What is left is genuinely platform bound or waits on ResourceHelper: SP, Storage, FileListProvider, the Dagger qualifiers, PasswordCheck, BlePreCheck, RfcommTransport, Sms, AapsSchedulers, and the plugin and pump interfaces that name ResourceHelper in a signature. Reified would read better than KClass at the call site. It can come later on its own - it is an inline extension, so it changes the shape of every call rather than one token. --- .../kotlin/app/aaps/CobExtendedCarbsTest.kt | 26 +++++++++---------- .../androidTest/kotlin/app/aaps/LoopTest.kt | 20 +++++++------- .../kotlin/app/aaps/helpers/RxHelper.kt | 11 ++++---- app/src/main/kotlin/app/aaps/MainApp.kt | 2 +- .../core/interfaces/bolus/BatchExecutor.kt | 0 .../interfaces/bolus/WizardBolusExecutor.kt | 0 .../core/interfaces/bolus/WizardExecutor.kt | 0 .../clientcontrol/ActionProgress.kt | 0 .../ClientControlActionDispatcher.kt | 0 .../interfaces/clientcontrol/PendingAction.kt | 0 .../app/aaps/core/interfaces/rx/bus/RxBus.kt | 3 ++- .../rx/events/EventAPSCalculationFinished.kt | 0 .../rx/events/EventAcceptOpenLoopChange.kt | 0 .../core/interfaces/rx/events/EventAppExit.kt | 0 .../rx/events/EventAppInitialized.kt | 0 .../EventAutosensCalculationFinished.kt | 0 .../interfaces/rx/events/EventBTChange.kt | 0 .../rx/events/EventBucketedDataCreated.kt | 0 .../rx/events/EventCalibrationChanged.kt | 0 .../rx/events/EventConcentrationChange.kt | 0 .../rx/events/EventConfigBuilderChange.kt | 0 .../rx/events/EventCustomActionsChanged.kt | 0 .../rx/events/EventDiaconnG8PumpLogReset.kt | 0 .../rx/events/EventInitializationChanged.kt | 0 .../core/interfaces/rx/events/EventLoop.kt | 0 .../rx/events/EventLoopUpdateGui.kt | 0 .../interfaces/rx/events/EventMobileToWear.kt | 0 .../rx/events/EventMobileToWearWatchface.kt | 0 .../rx/events/EventNewOpenLoopNotification.kt | 0 .../rx/events/EventNsClientStatusUpdated.kt | 0 .../interfaces/rx/events/EventNtpStatus.kt | 0 .../rx/events/EventProfileChangeRequested.kt | 0 .../rx/events/EventPumpStatusChanged.kt | 0 .../interfaces/rx/events/EventQueueChanged.kt | 0 .../rx/events/EventRefreshButtonState.kt | 0 .../rx/events/EventRefreshOverview.kt | 0 .../interfaces/rx/events/EventSWRLStatus.kt | 0 .../interfaces/rx/events/EventSWSyncStatus.kt | 0 .../interfaces/rx/events/EventSWUpdate.kt | 0 .../interfaces/rx/events/EventShowDialog.kt | 0 .../interfaces/rx/events/EventShowSnackbar.kt | 0 .../core/interfaces/rx/events/EventStatus.kt | 0 .../interfaces/rx/events/EventUpdateGui.kt | 0 .../events/EventUpdateOverviewCalcProgress.kt | 0 .../rx/events/EventUpdateSelectedWatchface.kt | 0 .../rx/events/EventWearDataToMobile.kt | 0 .../interfaces/rx/events/EventWearToMobile.kt | 0 .../rx/events/EventWearUpdateGui.kt | 0 .../rx/events/EventWearUpdateTiles.kt | 0 .../core/interfaces/rx/weardata/EventData.kt | 0 .../core/interfaces/scenes/SceneActions.kt | 0 .../interfaces/scenes/SceneAutomationApi.kt | 0 .../ui/compose/dialogs/GlobalDialogHost.kt | 2 +- .../ui/compose/dialogs/GlobalSnackbarHost.kt | 2 +- .../compose/pump/PumpCommunicationStatus.kt | 4 +-- .../compose/dialogs/GlobalDialogHostTest.kt | 3 ++- .../compose/dialogs/GlobalSnackbarHostTest.kt | 3 ++- .../queue/CommandQueueImplementation.kt | 2 +- .../ProfileSwitchExpirySchedulerTest.kt | 2 +- .../aps/autotune/compose/AutotuneViewModel.kt | 2 +- .../plugins/aps/compose/OpenAPSViewModel.kt | 4 +-- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 2 +- .../plugins/aps/loop/compose/LoopViewModel.kt | 4 +-- .../autotune/compose/AutotuneViewModelTest.kt | 2 +- .../aps/compose/OpenAPSComposeContentTest.kt | 4 +-- .../aps/compose/OpenAPSViewModelTest.kt | 4 +-- .../loop/compose/LoopComposeContentTest.kt | 4 +-- .../aps/loop/compose/LoopViewModelTest.kt | 4 +-- .../plugins/automation/AutomationRuntime.kt | 4 +-- .../compose/AutomationStateHolder.kt | 2 +- .../automation/services/LocationService.kt | 2 +- .../configuration/setupwizard/SWDefinition.kt | 8 +++--- .../setupwizard/SWEventListener.kt | 5 ++-- .../setupwizard/SetupWizardScreen.kt | 8 +++--- .../bgQualityCheck/BgQualityCheckPlugin.kt | 2 +- .../objectives/compose/ObjectivesViewModel.kt | 2 +- .../compose/ObjectivesViewModelTest.kt | 2 +- .../persistentNotification/DummyService.kt | 2 +- .../PersistentNotificationPlugin.kt | 12 ++++----- .../IobCobCalculatorPlugin.kt | 6 ++--- .../sync/nsclientV3/NSClientV3Plugin.kt | 2 +- .../services/RunningConfigurationPublisher.kt | 2 +- .../plugins/sync/tidepool/TidepoolPlugin.kt | 4 +-- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 4 +-- .../app/aaps/plugins/sync/wear/WearPlugin.kt | 12 ++++----- .../sync/wear/compose/WearViewModel.kt | 2 +- .../wear/wearintegration/DataHandlerMobile.kt | 8 +++--- .../DataLayerListenerServiceMobile.kt | 4 +-- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 6 ++--- .../sync/wear/compose/WearViewModelTest.kt | 2 +- .../DataHandlerMobileWearBolusTest.kt | 2 +- .../compose/ComboV2OverviewViewModelTest.kt | 4 +-- .../aaps/pump/common/PumpPluginAbstract.kt | 2 +- .../pump/dana/compose/DanaHistoryViewModel.kt | 2 +- .../dana/compose/DanaOverviewViewModel.kt | 4 +-- .../dana/compose/DanaHistoryViewModelTest.kt | 2 +- .../dana/compose/DanaOverviewViewModelTest.kt | 8 +++--- .../aaps/pump/danar/AbstractDanaRPlugin.kt | 2 +- .../kotlin/app/aaps/pump/danar/DanaRPlugin.kt | 2 +- .../danar/compose/DanaRPairWizardViewModel.kt | 4 +-- .../services/AbstractDanaRExecutionService.kt | 4 +-- .../pump/danarkorean/DanaRKoreanPlugin.kt | 2 +- .../app/aaps/pump/danarv2/DanaRv2Plugin.kt | 2 +- .../compose/DanaRPairWizardViewModelTest.kt | 4 +-- .../app/aaps/pump/danars/DanaRSPlugin.kt | 4 +-- .../pump/danars/services/DanaRSService.kt | 2 +- .../compose/DanaRSOverviewViewModelTest.kt | 8 +++--- .../app/aaps/pump/diaconn/DiaconnG8Plugin.kt | 6 ++--- .../compose/DiaconnHistoryViewModel.kt | 2 +- .../compose/DiaconnOverviewViewModel.kt | 4 +-- .../pump/diaconn/service/DiaconnG8Service.kt | 2 +- .../compose/DiaconnHistoryViewModelTest.kt | 2 +- .../compose/DiaconnOverviewViewModelTest.kt | 8 +++--- .../app/aaps/pump/eopatch/ble/PatchManager.kt | 2 +- .../compose/EopatchOverviewViewModelTest.kt | 4 +-- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 4 +-- .../equil/compose/EquilHistoryViewModel.kt | 2 +- .../equil/compose/EquilOverviewViewModel.kt | 4 +-- .../compose/EquilHistoryViewModelTest.kt | 2 +- .../compose/EquilOverviewViewModelTest.kt | 8 +++--- .../insight/compose/InsightComposeContent.kt | 2 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 2 +- .../compose/MedtronicOverviewViewModel.kt | 6 ++--- .../compose/MedtronicOverviewViewModelTest.kt | 10 +++---- .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 2 +- .../pump/medtrum/services/MedtrumService.kt | 2 +- .../compose/MedtrumOverviewViewModelTest.kt | 4 +-- .../dash/ui/compose/DashOverviewViewModel.kt | 2 +- .../ui/compose/DashOverviewViewModelTest.kt | 6 ++--- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 14 +++++----- .../eros/ui/compose/ErosOverviewViewModel.kt | 4 +-- .../ui/compose/ErosOverviewViewModelTest.kt | 8 +++--- .../compose/RileyLinkStatusViewModel.kt | 2 +- .../compose/RileyLinkStatusViewModelTest.kt | 2 +- .../pump/virtual/VirtualPumpViewModelTest.kt | 4 +-- .../app/aaps/shared/impl/rx/bus/RxBusImpl.kt | 3 ++- .../compose/loopSheet/LoopActionViewModel.kt | 8 +++--- .../ui/compose/manageSheet/ManageViewModel.kt | 4 +-- .../compose/overview/OverviewDataCacheImpl.kt | 10 +++---- .../overview/statusLights/StatusViewModel.kt | 6 ++--- .../QuickWizardManagementViewModel.kt | 2 +- .../ui/compose/scenes/SceneListViewModel.kt | 8 +++--- .../ui/compose/scenesSheet/ScenesViewModel.kt | 8 +++--- .../treatmentsSheet/TreatmentViewModel.kt | 2 +- .../loopSheet/LoopActionViewModelTest.kt | 8 +++--- .../manageSheet/ManageViewModelTest.kt | 4 +-- .../statusLights/StatusViewModelTest.kt | 6 ++--- .../QuickWizardManagementViewModelTest.kt | 2 +- .../compose/scenes/SceneListViewModelTest.kt | 8 +++--- .../scenesSheet/ScenesViewModelTest.kt | 8 +++--- .../treatmentsSheet/TreatmentViewModelTest.kt | 2 +- .../app/aaps/wear/comm/DataHandlerWear.kt | 2 +- .../wear/comm/DataLayerListenerServiceWear.kt | 6 ++--- .../activities/LoopStatusActivity.kt | 2 +- .../interaction/utils/MenuListActivity.kt | 2 +- .../aaps/wear/watchfaces/CircleWatchface.kt | 2 +- .../wear/watchfaces/utils/BaseWatchFace.kt | 2 +- .../app/aaps/wear/comm/DataHandlerWearTest.kt | 2 +- 158 files changed, 251 insertions(+), 245 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt (90%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt (100%) diff --git a/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt b/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt index 701920750029..4c3a641f4aeb 100644 --- a/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt @@ -88,7 +88,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { // ==================== Helpers ==================== private fun setupEnvironment() = runTest { - rxHelper.listen(EventAutosensCalculationFinished::class.java) + rxHelper.listen(EventAutosensCalculationFinished::class) l.findByName(LTag.EVENTS.name).enabled = true assertThat(config.APS).isTrue() @@ -185,7 +185,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { * 2. Autosens calculation to complete (replaces old EventNewHistoryData + EventAutosensCalculationFinished) */ private suspend fun insertBgAndWait(now: Long) { - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) // Insert BG and wait for the GV flow emission (replaces old EventNewBG) val gvList = waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { @@ -195,7 +195,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { assertThat(gvList).isNotEmpty() // Wait for autosens calculation triggered by BG insertion, then for the calc to fully settle - assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class.java, maxSeconds = 60, comment = "initial calc").first).isTrue() + assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class, maxSeconds = 60, comment = "initial calc").first).isTrue() waits.awaitCalculationFinished("initial calc settle") } @@ -203,7 +203,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { * Trigger recalculation by inserting a new BG and wait for autosens to complete. */ private suspend fun triggerCalculationAndWait(now: Long) { - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) val newBg = listOf( GV( @@ -216,7 +216,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { ) ) persistenceLayer.insertCgmSourceData(Sources.Random, newBg, emptyList(), null) - assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class.java, maxSeconds = 60, comment = "autosens").first).isTrue() + assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class, maxSeconds = 60, comment = "autosens").first).isTrue() waits.awaitCalculationFinished("autosens settle") } @@ -400,12 +400,12 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { setupEnvironment() val now = dateUtil.now() - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { insertFlatBgData(now, 240, 100.0) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 15 * 60_000L) - assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class.java, maxSeconds = 60, comment = "autosens").first).isTrue() + assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class, maxSeconds = 60, comment = "autosens").first).isTrue() waits.awaitCalculationFinished("absorption settle") assertCobBounded(10.0) @@ -417,12 +417,12 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { setupEnvironment() val now = dateUtil.now() - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { insertFlatBgData(now, 240, 100.0) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 0) - assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class.java, maxSeconds = 60, comment = "autosens").first).isTrue() + assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class, maxSeconds = 60, comment = "autosens").first).isTrue() waits.awaitCalculationFinished("absorption settle") assertCobBounded(10.0) @@ -436,7 +436,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { setupEnvironment() val now = dateUtil.now() - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { insertBgData(now, 60, { minutesAgo -> 200.0 - minutesAgo * (100.0 / 60.0) }, TrendArrow.FORTY_FIVE_UP) } @@ -452,7 +452,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { setupEnvironment() val now = dateUtil.now() - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { insertBgData(now, 60, { minutesAgo -> 180.0 - minutesAgo * (100.0 / 60.0) }, TrendArrow.FORTY_FIVE_UP) } @@ -468,12 +468,12 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { setupEnvironment() val now = dateUtil.now() - rxHelper.resetState(EventAutosensCalculationFinished::class.java) + rxHelper.resetState(EventAutosensCalculationFinished::class) waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { insertBgData(now, 240, { minutesAgo -> 250.0 - minutesAgo * (170.0 / 240.0) }, TrendArrow.FORTY_FIVE_UP) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 15 * 60_000L) - assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class.java, maxSeconds = 60, comment = "autosens").first).isTrue() + assertThat(rxHelper.waitFor(EventAutosensCalculationFinished::class, maxSeconds = 60, comment = "autosens").first).isTrue() waits.awaitCalculationFinished("absorption settle") assertCobBounded(10.0) diff --git a/app/src/androidTest/kotlin/app/aaps/LoopTest.kt b/app/src/androidTest/kotlin/app/aaps/LoopTest.kt index 1645410de23a..a494ec1aee64 100644 --- a/app/src/androidTest/kotlin/app/aaps/LoopTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/LoopTest.kt @@ -96,10 +96,10 @@ class LoopTest : HiltInstrumentedTest() { source = Sources.Aaps, listValues = listOf(ValueWithUnit.SimpleString("Migration")) ) - rxHelper.listen(EventLoopSetLastRunGui::class.java) - rxHelper.listen(EventResetOpenAPSGui::class.java) - rxHelper.listen(EventOpenAPSUpdateGui::class.java) - rxHelper.listen(EventAPSCalculationFinished::class.java) + rxHelper.listen(EventLoopSetLastRunGui::class) + rxHelper.listen(EventResetOpenAPSGui::class) + rxHelper.listen(EventOpenAPSUpdateGui::class) + rxHelper.listen(EventAPSCalculationFinished::class) objectivesPlugin.onStart() // Enable event logging @@ -110,7 +110,7 @@ class LoopTest : HiltInstrumentedTest() { // Loop should be limited by unfinished objectives loop.invoke("test1", allowNotification = false) - var loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class.java, comment = "step1") + var loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class, comment = "step1") assertThat(loopStatusEvent.first).isTrue() assertThat((loopStatusEvent.second as EventLoopSetLastRunGui).text).contains("Loop disabled by user") @@ -120,7 +120,7 @@ class LoopTest : HiltInstrumentedTest() { // Now there should be missing profile (profileFunction as ProfileFunctionImpl).cache.clear() loop.invoke("test2", allowNotification = false) - loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class.java, comment = "step2") + loopStatusEvent = rxHelper.waitFor(EventLoopSetLastRunGui::class, comment = "step2") assertThat(loopStatusEvent.first).isTrue() assertThat((loopStatusEvent.second as EventLoopSetLastRunGui).text).contains("NO PROFILE SET") @@ -156,13 +156,13 @@ class LoopTest : HiltInstrumentedTest() { assertThat(rxHelper.waitUntil("step3: pump profile set") { runBlocking { pumpSync.expectedPumpState() }.profile != null }).isTrue() // Loop should run — may get "NO APS SELECTED" (no glucose) or a real result (stale glucose cache) - rxHelper.listen(EventLoopUpdateGui::class.java) + rxHelper.listen(EventLoopUpdateGui::class) loop.invoke("test3", allowNotification = false) // Accept either: error event (no glucose) or update event (APS produced result from cached data) assertThat( rxHelper.waitUntil("step4: loop completed") { - rxHelper.waitFor(EventLoopSetLastRunGui::class.java, maxSeconds = 1, comment = "step4").first || - rxHelper.waitFor(EventLoopUpdateGui::class.java, maxSeconds = 1, comment = "step4").first + rxHelper.waitFor(EventLoopSetLastRunGui::class, maxSeconds = 1, comment = "step4").first || + rxHelper.waitFor(EventLoopUpdateGui::class, maxSeconds = 1, comment = "step4").first } ).isTrue() @@ -180,7 +180,7 @@ class LoopTest : HiltInstrumentedTest() { // GV insertion triggers calculation via observeChanges(GV) → scheduleHistoryDataChange (5s debounce) // The IOB/COB autosens phase may exit early ("No bucketed data") so EventAutosensCalculationFinished // is not guaranteed. Wait for EventAPSCalculationFinished which fires when loop runs. - assertThat(rxHelper.waitFor(EventAPSCalculationFinished::class.java, maxSeconds = 60, comment = "step6").first).isTrue() + assertThat(rxHelper.waitFor(EventAPSCalculationFinished::class, maxSeconds = 60, comment = "step6").first).isTrue() Thread.sleep(5000) assertThat(loop.lastRun).isNotNull() } diff --git a/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt b/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt index 2a919d7dbc7a..1093c42a97d8 100644 --- a/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt +++ b/app/src/androidTest/kotlin/app/aaps/helpers/RxHelper.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import java.util.concurrent.atomic.AtomicBoolean +import kotlin.reflect.KClass import javax.inject.Inject /** @@ -27,8 +28,8 @@ class RxHelper @Inject constructor( private val aapsLogger: AAPSLogger ) { - private val hashMap = HashMap, AtomicBoolean>() - private val eventHashMap = HashMap, Event>() + private val hashMap = HashMap, AtomicBoolean>() + private val eventHashMap = HashMap, Event>() // Lives as long as the helper; clear() cancels its collectors, like clearing the CompositeDisposable. private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -39,7 +40,7 @@ class RxHelper @Inject constructor( * @param clazz Class to observe * @return AtomicBoolean trigger */ - fun listen(clazz: Class): AtomicBoolean = + fun listen(clazz: KClass): AtomicBoolean = hashMap[clazz] ?: AtomicBoolean(false).also { ab -> hashMap[clazz] = ab // Setup RxBus tracking. UNDISPATCHED because RxBus has no replay: a test that sends an @@ -58,7 +59,7 @@ class RxHelper @Inject constructor( * @param clazz Class to observe * @param maxSeconds max waiting time in seconds */ - fun waitFor(clazz: Class, maxSeconds: Long = 40, comment: String = ""): Pair { + fun waitFor(clazz: KClass, maxSeconds: Long = 40, comment: String = ""): Pair { val watcher = hashMap[clazz] ?: error("Class not registered ${clazz.simpleName}") val start = dateUtil.now() while (!watcher.get()) { @@ -79,7 +80,7 @@ class RxHelper @Inject constructor( * * @param clazz Class */ - fun resetState(clazz: Class) { + fun resetState(clazz: KClass) { hashMap[clazz]?.set(false) eventHashMap.remove(clazz) } diff --git a/app/src/main/kotlin/app/aaps/MainApp.kt b/app/src/main/kotlin/app/aaps/MainApp.kt index 48d9fdf41793..ef8d73b5eaee 100644 --- a/app/src/main/kotlin/app/aaps/MainApp.kt +++ b/app/src/main/kotlin/app/aaps/MainApp.kt @@ -197,7 +197,7 @@ class MainApp : Application(), HasAndroidInjector, Configuration.Provider { // Visible activities host their own GlobalSnackbarHost that also // subscribes; those win while UI is present. appScope.launch { - rxBus.toFlow(EventShowSnackbar::class.java).collect { event -> + rxBus.toFlow(EventShowSnackbar::class).collect { event -> val uiVisible = ProcessLifecycleOwner.get().lifecycle.currentState .isAtLeast(Lifecycle.State.STARTED) if (!uiVisible) { diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/BatchExecutor.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/WizardBolusExecutor.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/bolus/WizardExecutor.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/ActionProgress.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/ClientControlActionDispatcher.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/clientcontrol/PendingAction.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt similarity index 90% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt index ed43d5d25fc9..ecce6be7fa7b 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/bus/RxBus.kt @@ -2,6 +2,7 @@ package app.aaps.core.interfaces.rx.bus import app.aaps.core.interfaces.rx.events.Event import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass /** * A simple event bus for communication between different parts of the application. @@ -25,5 +26,5 @@ interface RxBus { * @param eventType The class of the event to listen for. * @return A [Flow] that emits events of the specified type. */ - fun toFlow(eventType: Class): Flow + fun toFlow(eventType: KClass): Flow } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAPSCalculationFinished.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAcceptOpenLoopChange.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppExit.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAppInitialized.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventAutosensCalculationFinished.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventBTChange.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventBucketedDataCreated.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventCalibrationChanged.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventConcentrationChange.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventConfigBuilderChange.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventCustomActionsChanged.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventDiaconnG8PumpLogReset.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventInitializationChanged.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoop.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventLoopUpdateGui.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWear.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventMobileToWearWatchface.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNewOpenLoopNotification.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNsClientStatusUpdated.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventNtpStatus.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventProfileChangeRequested.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventPumpStatusChanged.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventQueueChanged.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshButtonState.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventRefreshOverview.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWRLStatus.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWSyncStatus.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventSWUpdate.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowDialog.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventShowSnackbar.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventStatus.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateGui.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateOverviewCalcProgress.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventUpdateSelectedWatchface.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearDataToMobile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearToMobile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateGui.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/events/EventWearUpdateTiles.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneActions.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/scenes/SceneAutomationApi.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt index 78db977381c9..0872f048585b 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt @@ -44,7 +44,7 @@ fun GlobalDialogHost(rxBus: RxBus) { LaunchedEffect(rxBus, lifecycleOwner) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - rxBus.toFlow(EventShowDialog::class.java).collect { event -> + rxBus.toFlow(EventShowDialog::class).collect { event -> val choice = CompletableDeferred() try { current = event to choice diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt index 108b6b553d33..5c4ff375f5c9 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt @@ -66,7 +66,7 @@ fun GlobalSnackbarHost( // transition, double-surfacing messages. LaunchedEffect(rxBus, lifecycleOwner) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - rxBus.toFlow(EventShowSnackbar::class.java).collect { event -> + rxBus.toFlow(EventShowSnackbar::class).collect { event -> hostState.showSnackbar( BusSnackbarVisuals(message = event.message, type = event.type) ) diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt index d910a6792927..f9eabfec6992 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt @@ -38,7 +38,7 @@ class PumpCommunicationStatus( val refreshTrigger: MutableStateFlow = MutableStateFlow(0L) init { - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .onEach { event -> val text = rh.gs(event.getStatus()) _statusBanner.value = if (text.isEmpty()) null else StatusBanner(text = text, level = StatusLevel.UNSPECIFIED) @@ -46,7 +46,7 @@ class PumpCommunicationStatus( } .launchIn(scope) - rxBus.toFlow(EventQueueChanged::class.java) + rxBus.toFlow(EventQueueChanged::class) .onEach { _queueStatus.value = commandQueue.statusAsAnnotated().takeIf { it.isNotEmpty() } refreshTrigger.value = System.currentTimeMillis() diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt index 263162479dfe..d621bfcc5121 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.rx.events.EventShowDialog import app.aaps.core.ui.R import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import org.junit.Before @@ -143,7 +144,7 @@ class GlobalDialogHostTest { @Suppress("UNCHECKED_CAST") - override fun toFlow(eventType: Class): Flow = + override fun toFlow(eventType: KClass): Flow = events.filter { eventType.isInstance(it) } as Flow } } diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt index e4673e514d72..d605d94d8d2e 100644 --- a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt +++ b/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt @@ -13,6 +13,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.LocalPreferences import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter @@ -75,7 +76,7 @@ class GlobalSnackbarHostTest { @Suppress("UNCHECKED_CAST") - override fun toFlow(eventType: Class): Flow = + override fun toFlow(eventType: KClass): Flow = events.filter { eventType.isInstance(it) } as Flow } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index 3ba01ddf9b44..315fb8a70024 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -137,7 +137,7 @@ class CommandQueueImplementation @Inject constructor( // the event carries it directly (scene revert), while a bare PS DB change (scene start / anyone else) // consumes the one-shot ProfileSwitchSilentGate flag the scene set just before inserting its PS. merge( - rxBus.toFlow(EventProfileChangeRequested::class.java).map { it.silent }, + rxBus.toFlow(EventProfileChangeRequested::class).map { it.silent }, persistenceLayer.observeChanges(PS::class.java).map { profileSwitchSilentGate.consumeSilent() } ).collectResilient(appScope, aapsLogger, LTag.PROFILE) { silent -> onProfileChanged(silent) } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt index baff9bcf75db..d57ac9cf5d47 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt @@ -54,7 +54,7 @@ class ProfileSwitchExpirySchedulerTest : TestBase() { // UNDISPATCHED so the collector is subscribed before the scheduler under test sends anything; // RxBus has no replay, so a scheduled collector would miss those events. collector = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { - rxBus.toFlow(EventProfileChangeRequested::class.java).collect { eventCount++ } + rxBus.toFlow(EventProfileChangeRequested::class).collect { eventCount++ } } scheduler = ProfileSwitchExpiryScheduler( persistenceLayer = persistenceLayer, diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt index e612b894b520..a0a0b6cb9127 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModel.kt @@ -182,7 +182,7 @@ class AutotuneViewModel( if (autotunePlugin.lastNbDays.isEmpty()) autotunePlugin.lastNbDays = preferences.get(IntKey.AutotuneDefaultTuneDays).toString() - rxBus.toFlow(EventAutotuneUpdateGui::class.java) + rxBus.toFlow(EventAutotuneUpdateGui::class) .onEach { refreshState() } .launchIn(scope) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModel.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModel.kt index 7edb113b6c94..0ea1329817b1 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModel.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModel.kt @@ -58,11 +58,11 @@ class OpenAPSViewModel( val uiState: StateFlow = _uiState.asStateFlow() init { - rxBus.toFlow(EventOpenAPSUpdateGui::class.java) + rxBus.toFlow(EventOpenAPSUpdateGui::class) .onEach { updateState() } .launchIn(scope) - rxBus.toFlow(EventResetOpenAPSGui::class.java) + rxBus.toFlow(EventResetOpenAPSGui::class) .onEach { event -> resetState(event.text) } .launchIn(scope) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index b7ef9506eaa9..fc89e1bb49d7 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -193,7 +193,7 @@ class LoopPlugin @Inject constructor( // exactly when isSuspended() may have flipped. runningModePreCheck() is idempotent (writes only when // isSuspended() and the RM mode disagree, APS-gated) and emits only EventRefreshOverview — never // EventPumpStatusChanged — so there is no feedback loop. The debounce collapses connection chatter. - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .debounce(1000L) .onEach { try { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModel.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModel.kt index e392a5aef7b4..cb5c05234aee 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModel.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModel.kt @@ -53,11 +53,11 @@ class LoopViewModel( val uiState: StateFlow = _uiState.asStateFlow() init { - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .onEach { updateState() } .launchIn(scope) - rxBus.toFlow(EventLoopSetLastRunGui::class.java) + rxBus.toFlow(EventLoopSetLastRunGui::class) .onEach { event -> _uiState.value = LoopUiState(statusMessage = event.text) } diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModelTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModelTest.kt index 46a3640d9286..bace97567718 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModelTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/autotune/compose/AutotuneViewModelTest.kt @@ -85,7 +85,7 @@ internal class AutotuneViewModelTest { whenever(autotunePlugin.lastNbDays).thenReturn("5") // non-empty -> skips the default-days assignment whenever(autotunePlugin.result).thenReturn("") // .isEmpty() in checkNewDay() whenever(autotunePlugin.lastRun).thenReturn(Long.MAX_VALUE) // forces runToday true -> resetParam(false), off the days path - whenever(rxBus.toFlow(EventAutotuneUpdateGui::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventAutotuneUpdateGui::class)).thenReturn(emptyFlow()) // StandardTestDispatcher-backed scope: scope.launch { refreshState() } and the rxBus flow // collection stay queued (never advanced), so the default uiState is what we assert against. diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSComposeContentTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSComposeContentTest.kt index 3d09a93f9fea..318422b6ea56 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSComposeContentTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSComposeContentTest.kt @@ -37,8 +37,8 @@ class OpenAPSComposeContentTest { fun render_showsNotAvailable_whenNoApsResult() { val apsPlugin = mock { whenever(it.lastAPSResult).thenReturn(null) } val rxBus = mock { - whenever(it.toFlow(EventOpenAPSUpdateGui::class.java)).thenReturn(emptyFlow()) - whenever(it.toFlow(EventResetOpenAPSGui::class.java)).thenReturn(emptyFlow()) + whenever(it.toFlow(EventOpenAPSUpdateGui::class)).thenReturn(emptyFlow()) + whenever(it.toFlow(EventResetOpenAPSGui::class)).thenReturn(emptyFlow()) } val rh = mock { whenever(it.gs(R.string.not_available_full)).thenReturn("N/A") } val content = OpenAPSComposeContent(apsPlugin, rxBus, rh, mock()) diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModelTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModelTest.kt index f5f0fba82ebc..1d9ea0e3596b 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModelTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/compose/OpenAPSViewModelTest.kt @@ -30,8 +30,8 @@ internal class OpenAPSViewModelTest { @BeforeEach fun setUp() { MockitoAnnotations.openMocks(this) - whenever(rxBus.toFlow(EventOpenAPSUpdateGui::class.java)).thenReturn(updateGuiFlow) - whenever(rxBus.toFlow(EventResetOpenAPSGui::class.java)).thenReturn(resetGuiFlow) + whenever(rxBus.toFlow(EventOpenAPSUpdateGui::class)).thenReturn(updateGuiFlow) + whenever(rxBus.toFlow(EventResetOpenAPSGui::class)).thenReturn(resetGuiFlow) } // Dispatchers.Unconfined runs the VM's launched coroutines (init updateState + onRefresh's diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopComposeContentTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopComposeContentTest.kt index 5a90729bbfd0..6b7db3337ae8 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopComposeContentTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopComposeContentTest.kt @@ -40,8 +40,8 @@ class LoopComposeContentTest { fun render_showsNotAvailable_whenNoLastRun() { val loop = mock { whenever(it.lastRun).thenReturn(null) } val rxBus = mock { - whenever(it.toFlow(EventLoopUpdateGui::class.java)).thenReturn(emptyFlow()) - whenever(it.toFlow(EventLoopSetLastRunGui::class.java)).thenReturn(emptyFlow()) + whenever(it.toFlow(EventLoopUpdateGui::class)).thenReturn(emptyFlow()) + whenever(it.toFlow(EventLoopSetLastRunGui::class)).thenReturn(emptyFlow()) } val rh = mock { whenever(it.gs(R.string.not_available_full)).thenReturn("N/A") } val content = LoopComposeContent( diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModelTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModelTest.kt index 4733d9ec2bcc..0e11b9d5a24e 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModelTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/compose/LoopViewModelTest.kt @@ -36,8 +36,8 @@ internal class LoopViewModelTest { @BeforeEach fun setUp() { MockitoAnnotations.openMocks(this) - whenever(rxBus.toFlow(EventLoopUpdateGui::class.java)).thenReturn(updateGuiFlow) - whenever(rxBus.toFlow(EventLoopSetLastRunGui::class.java)).thenReturn(lastRunGuiFlow) + whenever(rxBus.toFlow(EventLoopUpdateGui::class)).thenReturn(updateGuiFlow) + whenever(rxBus.toFlow(EventLoopSetLastRunGui::class)).thenReturn(lastRunGuiFlow) } // Dispatchers.Unconfined runs the VM's launched coroutines (init's updateState) eagerly and diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt index c36d539b3b36..311df0254b65 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt @@ -358,12 +358,12 @@ class AutomationRuntime @Inject constructor( // fired scope.launch and returned. That may well be an improvement, but changing when // automation rules can run concurrently is not something to do as a side effect of swapping // the subscription mechanism. - rxBus.toFlow(EventLocationChange::class.java) + rxBus.toFlow(EventLocationChange::class) .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.AUTOMATION, "Grabbed location: ${it.location.latitude} ${it.location.longitude} Provider: ${it.location.provider}") scope?.launch { processActions() } } - rxBus.toFlow(EventBTChange::class.java) + rxBus.toFlow(EventBTChange::class) .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.AUTOMATION, "Grabbed new BT event: $it") btConnects.add(it) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt index d1bcec048b17..ce938bc823a7 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt @@ -58,7 +58,7 @@ class AutomationStateHolder( scope = newScope // This one observed on aapsSchedulers.main and the scope is already Main, so the dispatcher // matches without any extra step - it touches Compose state. - rxBus.toFlow(EventAutomationUpdateGui::class.java) + rxBus.toFlow(EventAutomationUpdateGui::class) .collectResilient(newScope, aapsLogger, LTag.AUTOMATION, start = CoroutineStart.UNDISPATCHED) { refresh() refreshEditState() diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt index 0f5f9fe21fb6..018f0d2b7021 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/services/LocationService.kt @@ -151,7 +151,7 @@ class LocationService : DaggerService() { rxBus.send(EventShowSnackbar(getString(app.aaps.core.ui.R.string.location_permission_not_granted), EventShowSnackbar.Type.Error)) } - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.LOCATION, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.LOCATION, "EventAppExit received") stopSelf() diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt index 3f7851d735aa..abec91f69a9b 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWDefinition.kt @@ -121,7 +121,7 @@ class SWDefinition @Inject constructor( // so this subscription was already app-lifetime. Plain onEach/launchIn rather than // collectResilient because there is no logger here and the body is a bus send that cannot // meaningfully fail - the Rx version had no error handler either. - rxBus.toFlow(EventConfigBuilderChange::class.java) + rxBus.toFlow(EventConfigBuilderChange::class) .onEach { rxBus.send(EventSWUpdate(true)) } .launchIn(appScope) } @@ -205,7 +205,7 @@ class SWDefinition @Inject constructor( .add(swBreakProvider.get()) .add(swInfoTextProvider.get().label(R.string.syncinfotext)) .add(swBreakProvider.get()) - .add(swEventListenerProvider.get().with(EventSWSyncStatus::class.java).label(R.string.status_label).initialStatus(nsClient.status)) + .add(swEventListenerProvider.get().with(EventSWSyncStatus::class).label(R.string.status_label).initialStatus(nsClient.status)) .validator { nsClient.connected && nsClient.hasWritePermission } // Master side: explain the paired client-control channel, open the pairing (Authorized clients) screen, @@ -335,7 +335,7 @@ class SWDefinition @Inject constructor( .visibility { activePlugin.activePumpInternal.let { it is OmnipodEros && !it.isRileyLinkReady() } } ) .add( // Omnipod Eros only - swEventListenerProvider.get().with(EventSWRLStatus::class.java) + swEventListenerProvider.get().with(EventSWRLStatus::class) .label(R.string.setupwizard_pump_riley_link_status) .visibility { activePlugin.activePumpInternal is OmnipodEros }) .add( @@ -348,7 +348,7 @@ class SWDefinition @Inject constructor( activePlugin.activePump !is OmnipodEros && activePlugin.activePump !is OmnipodDash && activePlugin.activePump !is Medtrum }) .add( - swEventListenerProvider.get().with(EventPumpStatusChanged::class.java) + swEventListenerProvider.get().with(EventPumpStatusChanged::class) .visibility { activePlugin.activePumpInternal !is OmnipodEros && activePlugin.activePumpInternal !is OmnipodDash && activePlugin.activePumpInternal !is Medtrum }) .validator { isPumpInitialized() } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt index 20716b51b97f..561e82d8b379 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SWEventListener.kt @@ -16,6 +16,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.stringResource import app.aaps.plugins.configuration.setupwizard.elements.SWItem +import kotlin.reflect.KClass import javax.inject.Inject class SWEventListener @Inject constructor( @@ -30,9 +31,9 @@ class SWEventListener @Inject constructor( private var status = "" private var visibilityValidator: (() -> Boolean)? = null - lateinit var clazz: Class + lateinit var clazz: KClass - fun with(clazz: Class): SWEventListener { + fun with(clazz: KClass): SWEventListener { this.clazz = clazz return this } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt index 915946ac1cd1..6780e7ff2913 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/setupwizard/SetupWizardScreen.kt @@ -112,10 +112,10 @@ fun SetupWizardScreen( // screen leaves - the same thing onDispose was doing. LaunchedEffect(Unit) { merge( - rxBus.toFlow(EventSWUpdate::class.java), - rxBus.toFlow(EventPumpStatusChanged::class.java), - rxBus.toFlow(EventSWRLStatus::class.java), - rxBus.toFlow(EventSWSyncStatus::class.java) + rxBus.toFlow(EventSWUpdate::class), + rxBus.toFlow(EventPumpStatusChanged::class), + rxBus.toFlow(EventSWRLStatus::class), + rxBus.toFlow(EventSWSyncStatus::class) ).collect { updateTick++ } } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt index fd782e0e8e87..9433fbf1dad6 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt @@ -54,7 +54,7 @@ class BgQualityCheckPlugin @Inject constructor( // CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay: a scheduled // collector could miss data created before it starts. val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO).also { this.scope = it } - rxBus.toFlow(EventBucketedDataCreated::class.java) + rxBus.toFlow(EventBucketedDataCreated::class) .collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { processBgData() } } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModel.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModel.kt index a598d4b763ee..225f2df41bfa 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModel.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModel.kt @@ -70,7 +70,7 @@ class ObjectivesViewModel @Inject constructor( } init { - rxBus.toFlow(EventObjectivesUpdateGui::class.java) + rxBus.toFlow(EventObjectivesUpdateGui::class) .onEach { updateState() } .launchIn(scope) diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModelTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModelTest.kt index 3da6b9bece8f..eb25f59fe83a 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModelTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/objectives/compose/ObjectivesViewModelTest.kt @@ -48,7 +48,7 @@ internal class ObjectivesViewModelTest { // scope.launch { updateState() } + the update timer are deferred by StandardTestDispatcher, so the // objectivesPlugin.objectives list is never enumerated at construction; only the init flow chains are built. Dispatchers.setMain(StandardTestDispatcher()) - whenever(rxBus.toFlow(EventObjectivesUpdateGui::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventObjectivesUpdateGui::class)).thenReturn(emptyFlow()) // The 8 completion-tracking prefs are observed (.drop(1).merge()) in init — each must be non-null. listOf( BooleanNonKey.ObjectivesBgIsAvailableInNs, BooleanNonKey.ObjectivesPumpStatusIsAvailableInNS, diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt index ebec21153e96..5c2beab5d95d 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/DummyService.kt @@ -49,7 +49,7 @@ class DummyService : DaggerService() { } catch (e: Exception) { startForeground(4711, Notification()) } - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.CORE, "EventAppExit received") stopSelf() diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index ddda0054538b..4d975a1d1c21 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -111,11 +111,11 @@ class PersistentNotificationPlugin @Inject constructor( notificationHolder.createNotificationChannel() val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventRefreshOverview::class.java) + rxBus.toFlow(EventRefreshOverview::class) .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } - rxBus.toFlow(EventAutosensCalculationFinished::class.java) + rxBus.toFlow(EventAutosensCalculationFinished::class) .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { triggerNotificationUpdate() } /// Android Auto - debounced to prevent rapid pop-ups // Flow's debounce means the same thing as Rx's: emit once the source has been quiet for the @@ -123,9 +123,9 @@ class PersistentNotificationPlugin @Inject constructor( // coroutines that are dispatched, so the window survives it. Harmless for this one: the worst // case is a notification refresh that a later event triggers anyway. merge( - rxBus.toFlow(EventRefreshOverview::class.java).map { }, - rxBus.toFlow(EventInitializationChanged::class.java).map { }, - rxBus.toFlow(EventAutosensCalculationFinished::class.java).map { } + rxBus.toFlow(EventRefreshOverview::class).map { }, + rxBus.toFlow(EventInitializationChanged::class).map { }, + rxBus.toFlow(EventAutosensCalculationFinished::class).map { } ) .debounce(10_000L) .collectResilient(newScope, aapsLogger, LTag.CORE) { triggerNotificationUpdate(includeAuto = true) } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt index 94fc8978fe91..152e1b5df154 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt @@ -116,12 +116,12 @@ class IobCobCalculatorPlugin @Inject constructor( val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope // EventConfigBuilderChange - rxBus.toFlow(EventConfigBuilderChange::class.java) + rxBus.toFlow(EventConfigBuilderChange::class) .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { resetDataAndRunCalculation("onEventConfigBuilderChange") } // EventCalibrationChanged → the fit changed, so bucketed data needs to be re-smoothed // with the new calibration applied. scheduleHistoryDataChange has its own 5s debounce // so bursts (delete-many, bulk-add) collapse into one workflow run. - rxBus.toFlow(EventCalibrationChanged::class.java) + rxBus.toFlow(EventCalibrationChanged::class) .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { val invalidateFrom = System.currentTimeMillis() - T.hours(24).msecs() scheduleHistoryDataChange(invalidateFrom, reloadBgData = true, triggeredByNewBG = false) @@ -174,7 +174,7 @@ class IobCobCalculatorPlugin @Inject constructor( }.launchIn(newScope) // EventAppInitialized fires once, early. UNDISPATCHED matters most here of the three: a // scheduled collector could miss it outright and the main calculation would never be kicked off. - rxBus.toFlow(EventAppInitialized::class.java) + rxBus.toFlow(EventAppInitialized::class) .collectResilient(newScope, aapsLogger, LTag.AUTOSENS, start = CoroutineStart.UNDISPATCHED) { calculationWorkflow.runCalculation( CalculationWorkflow.MAIN_CALCULATION, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index d38c38c4bda4..1e20244eab6c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -300,7 +300,7 @@ class NSClientV3Plugin @Inject constructor( wsConnectedFlow.collect { connected -> if (connected) requestMasterProbe() } } } - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.NSCLIENT) { stopService() WorkManager.getInstance(context).cancelUniqueWork(JOB_NAME) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt index 3772f303ccd5..dc8a019c64fa 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/RunningConfigurationPublisher.kt @@ -114,7 +114,7 @@ class RunningConfigurationPublisher @Inject constructor( } } - val switchTrigger: Flow = rxBus.toFlow(EventConfigBuilderChange::class.java).map { } + val switchTrigger: Flow = rxBus.toFlow(EventConfigBuilderChange::class).map { } // Authorized-clients changes (pair / unpair / revoke / markActive) republish so paired // clients can read the current roster and detect when they've been orphaned. val authorizedClientsTrigger: Flow = authorizedRepository.observe().map { } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index 25648c3b1614..81b76c013176 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -97,9 +97,9 @@ class TidepoolPlugin @Inject constructor( if (isAllowed) doUpload("CONNECTIVITY") } // scope is Dispatchers.IO, matching the scheduler these subscriptions used before. - rxBus.toFlow(EventTidepoolDoUpload::class.java) + rxBus.toFlow(EventTidepoolDoUpload::class) .collectResilient(scope, aapsLogger, LTag.TIDEPOOL, start = CoroutineStart.UNDISPATCHED) { doUpload(EventTidepoolDoUpload::class.simpleName) } - rxBus.toFlow(EventTidepoolStatus::class.java) + rxBus.toFlow(EventTidepoolStatus::class) .collectResilient(scope, aapsLogger, LTag.TIDEPOOL, start = CoroutineStart.UNDISPATCHED) { event -> tidepoolRepository.addLog(event.status) tidepoolRepository.updateConnectionStatus(authFlowOut.connectionStatus) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index f5ae295fce66..4880ee7d05ca 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -82,9 +82,9 @@ class TizenPlugin @Inject constructor( val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope // newScope is Dispatchers.IO, matching the scheduler these subscriptions used before. - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { sendData(it) } - rxBus.toFlow(EventAutosensCalculationFinished::class.java) + rxBus.toFlow(EventAutosensCalculationFinished::class) .collectResilient(newScope, aapsLogger, LTag.CORE, start = CoroutineStart.UNDISPATCHED) { sendData(it) } bolusProgressData.state .collectResilient(newScope, aapsLogger, LTag.CORE) { state -> diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt index 7ed28fc89f13..df7aa697dd48 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt @@ -146,15 +146,15 @@ class WearPlugin @Inject constructor( dataHandlerMobile.resendData("PreferenceChange") checkCustomWatchfacePreferences() } - rxBus.toFlow(EventAutosensCalculationFinished::class.java) + rxBus.toFlow(EventAutosensCalculationFinished::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventAutosensCalculationFinished") } - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventLoopUpdateGui") } // AAPSCLIENT: fresh predictions arrive via NS devicestatus, not a local loop run — without this the // watch graph trails the phone by one loop cycle (the BG-triggered autosens resend fires BEFORE the // master's new devicestatus lands). Event is only sent on AAPSCLIENT; processedDeviceStatusData is // updated synchronously before it fires, so the resend reads the new predictions. - rxBus.toFlow(EventNsClientStatusUpdated::class.java) + rxBus.toFlow(EventNsClientStatusUpdated::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.resendData("EventNsClientStatusUpdated") } // Push status to watch quickly when a TT changes, without waiting for the loop's 10s debounce persistenceLayer.observeChanges() @@ -168,9 +168,9 @@ class WearPlugin @Inject constructor( // Push active-scene flag to wear so the tile can swap between scene list and STOP button scenes.activeFlow .collectResilient(newScope, aapsLogger, LTag.WEAR) { dataHandlerMobile.sendActiveSceneState(it) } - rxBus.toFlow(EventWearUpdateTiles::class.java) + rxBus.toFlow(EventWearUpdateTiles::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { dataHandlerMobile.sendUserActions() } - rxBus.toFlow(EventWearUpdateGui::class.java) + rxBus.toFlow(EventWearUpdateGui::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> // This one observed on aapsSchedulers.main, not io: it writes the watchface StateFlow the // UI reads and then walks preferences. newScope is IO, so the body is put back on main @@ -184,7 +184,7 @@ class WearPlugin @Inject constructor( } } } - rxBus.toFlow(EventMobileToWear::class.java) + rxBus.toFlow(EventMobileToWear::class) .collectResilient(newScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { // If there is a broadcast selected (ie. // AAPSClient want pass data to AAPS diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt index 7a6dba28bb7d..54705eab3a51 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModel.kt @@ -135,7 +135,7 @@ class WearViewModel @Inject constructor( } } viewModelScope.launch { - rxBus.toFlow(EventWearUpdateGui::class.java).collect { event -> + rxBus.toFlow(EventWearUpdateGui::class).collect { event -> if (event.exportFile) { _toastEvent.emit(rh.gs(R.string.wear_new_custom_watchface_exported)) } else { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index 0b37673917e9..926fb65cb0ff 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -179,7 +179,7 @@ class DataHandlerMobile @Inject constructor( // // UNDISPATCHED is required, not cosmetic: these subscribe from init on a replay-0 bus, so a // scheduled collector would drop anything sent before it started. - rxBus.toFlow(T::class.java) + rxBus.toFlow(T::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${event.sourceNodeId}") handler(event) @@ -195,7 +195,7 @@ class DataHandlerMobile @Inject constructor( crossinline detail: (T) -> String = { "" }, crossinline handler: (T) -> Unit ) { - rxBus.toFlow(T::class.java) + rxBus.toFlow(T::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${it.sourceNodeId}${detail(it)}") handler(it) @@ -341,10 +341,10 @@ class DataHandlerMobile @Inject constructor( // The collector is sequential, so batches are still handled one after another the way // concatMapCompletable did, and collectResilient logs and continues like doOnError + // onErrorComplete. - rxBus.toFlow(EventData.ActionHeartRate::class.java) + rxBus.toFlow(EventData.ActionHeartRate::class) .chunkedOnQuietPeriod(HEALTH_EVENT_QUIET_PERIOD_MS) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { handleHeartRateBatch(it) } - rxBus.toFlow(EventData.ActionStepsRate::class.java) + rxBus.toFlow(EventData.ActionStepsRate::class) .chunkedOnQuietPeriod(HEALTH_EVENT_QUIET_PERIOD_MS) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { handleStepsCountBatch(it) } onEventSync(detail = { " watchface=${it.customWatchface}" }) { handleGetCustomWatchface(it) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt index 2f6ca0df605a..c55043130041 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -74,11 +74,11 @@ class DataLayerListenerServiceMobile : WearableListenerService() { // scope is Main.immediate and onDestroy cancels it, so it is the right lifetime - but these two // observed on aapsSchedulers.io, and sendMessage talks to the Wear Data Layer. So the collector // lives on the service's scope and the send goes back to IO. - rxBus.toFlow(EventMobileToWear::class.java) + rxBus.toFlow(EventMobileToWear::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { withContext(Dispatchers.IO) { sendMessage(rxPath, it.payload.serialize()) } } - rxBus.toFlow(EventMobileToWearWatchface::class.java) + rxBus.toFlow(EventMobileToWearWatchface::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { withContext(Dispatchers.IO) { sendMessage(rxWatchfacePath, it.payload) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 411c2b790af4..3af201384c44 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -128,7 +128,7 @@ class XdripPlugin @Inject constructor( super.onStart() handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) // scope is Dispatchers.IO, which is what observeOn(aapsSchedulers.io) gave these before. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { WorkManager.getInstance(context).cancelUniqueWork(XDRIP_JOB_NAME) } persistenceLayer.observeAnyChange() // HR/SC writes come from the watch; this plugin doesn't broadcast them — skip to avoid reconnect-flush storm. @@ -137,9 +137,9 @@ class XdripPlugin @Inject constructor( sendStatusLine() delayAndScheduleExecution("DB_CHANGED(${types.joinToString { it.simpleName ?: "?" }})") } - rxBus.toFlow(EventAutosensCalculationFinished::class.java) + rxBus.toFlow(EventAutosensCalculationFinished::class) .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { sendStatusLine() } - rxBus.toFlow(EventAppInitialized::class.java) + rxBus.toFlow(EventAppInitialized::class) .collectResilient(scope, aapsLogger, LTag.XDRIP, start = CoroutineStart.UNDISPATCHED) { sendStatusLine() } eventWorker = Executors.newSingleThreadScheduledExecutor() } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt index 66a9ae9a1f96..eebf632b1655 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/compose/WearViewModelTest.kt @@ -59,7 +59,7 @@ internal class WearViewModelTest { Dispatchers.setMain(UnconfinedTestDispatcher()) whenever(wearPlugin.connectedDevice).thenReturn(connectedDeviceFlow) whenever(wearPlugin.savedCustomWatchface).thenReturn(savedCustomWatchfaceFlow) - whenever(rxBus.toFlow(EventWearUpdateGui::class.java)).thenReturn(eventWearUpdateGuiFlow) + whenever(rxBus.toFlow(EventWearUpdateGui::class)).thenReturn(eventWearUpdateGuiFlow) whenever(rh.gs(R.string.no_watch_connected)).thenReturn("No watch connected") sut = WearViewModel(wearPlugin, rxBus, rh, dateUtil, preferences, versionCheckerUtils, fileListProvider, aapsLogger) } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt index 7abc3b32a69b..2f3add69e42f 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobileWearBolusTest.kt @@ -114,7 +114,7 @@ class DataHandlerMobileWearBolusTest : TestBaseWithProfile() { */ private fun collectMobileToWear(onPayload: (EventData) -> Unit): Job = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { - rxBus.toFlow(EventMobileToWear::class.java).collect { onPayload(it.payload) } + rxBus.toFlow(EventMobileToWear::class).collect { onPayload(it.payload) } } /** Capture the single [ConfirmAction] the handler ships to the watch via [EventMobileToWear]. */ private inline fun capturedConfirm(block: () -> Unit): EventData.ConfirmAction { diff --git a/pump/combov2/src/test/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModelTest.kt b/pump/combov2/src/test/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModelTest.kt index fe7d6bc835c2..3a7c5cd14dd7 100644 --- a/pump/combov2/src/test/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModelTest.kt +++ b/pump/combov2/src/test/kotlin/info/nightscout/pump/combov2/compose/ComboV2OverviewViewModelTest.kt @@ -65,8 +65,8 @@ internal class ComboV2OverviewViewModelTest { Dispatchers.setMain(testDispatcher) // rx wiring launched by the PumpCommunicationStatus field at construction. - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) // The snapshotFlow / displayFrame field-initializers read every *UIFlow getter at // construction, so each must return a real StateFlow or construction NPEs. Defaults model an diff --git a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt index a0ad92290258..7e2ce4f3af37 100644 --- a/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt +++ b/pump/common/src/main/kotlin/app/aaps/pump/common/PumpPluginAbstract.kt @@ -124,7 +124,7 @@ abstract class PumpPluginAbstract protected constructor( // scheduled collector could miss an exit sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(serviceConnection!!) } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt index 23e997908120..d74657fd887d 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModel.kt @@ -79,7 +79,7 @@ class DanaHistoryViewModel @Inject constructor( // Listen for sync status. viewModelScope is Main, like observeOn(aapsSchedulers.main), and // dies with the view model like the CompositeDisposable did. UNDISPATCHED because RxBus has // no replay, so a scheduled collector could miss a status sent before it starts. - rxBus.toFlow(EventDanaRSyncStatus::class.java) + rxBus.toFlow(EventDanaRSyncStatus::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> _uiState.update { it.copy(statusMessage = event.message) } } diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt index f97e0dfdfea4..e5d4200bf118 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt @@ -96,9 +96,9 @@ open class DanaOverviewViewModel @Inject constructor( // viewModelScope dies with the view model like the CompositeDisposable did. The bodies only // write a timestamp, so the dispatcher does not matter. UNDISPATCHED because RxBus has no // replay, so a scheduled collector could miss an event sent before it starts. - rxBus.toFlow(EventDanaRNewStatus::class.java) + rxBus.toFlow(EventDanaRNewStatus::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } // Observe EB/TB database changes for immediate UI updates diff --git a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt index 19ba04fdf4e1..cc3aa854dac2 100644 --- a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt +++ b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaHistoryViewModelTest.kt @@ -78,7 +78,7 @@ internal class DanaHistoryViewModelTest { whenever(rh.gs(anyInt())).thenReturn("") // rx wiring touched at construction - whenever(rxBus.toFlow(EventDanaRSyncStatus::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventDanaRSyncStatus::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) diff --git a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt index e7834e48a9d1..9e3f500cb34b 100644 --- a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt +++ b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt @@ -85,10 +85,10 @@ internal class DanaOverviewViewModelTest { whenever(danaPump.temporaryBasalToString()).thenReturn("") // DanaPump method, read in buildUiState // rx wiring touched at construction - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventDanaRNewStatus::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt index 49ae129a9f17..3e3200994dbb 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt @@ -103,7 +103,7 @@ abstract class AbstractDanaRPlugin protected constructor( // Same scope as the preference observer below: IO, like the io scheduler used before, and // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus // has no replay, so a scheduled collector could miss a change sent before it starts. - rxBus.toFlow(EventConfigBuilderChange::class.java) + rxBus.toFlow(EventConfigBuilderChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { danaPump.reset() } preferences.observe(DanaStringNonKey.RName).drop(1).onEach { diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt index 58a843895804..0af7396160e4 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt @@ -117,7 +117,7 @@ class DanaRPlugin @Inject constructor( // Same scope as the preference observer above: IO, like the io scheduler used before, and // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus // has no replay, so a scheduled collector could miss an exit sent before it starts. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt index b3551657c3f7..d2409b9db595 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModel.kt @@ -85,9 +85,9 @@ class DanaRPairWizardViewModel @Inject constructor( // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay, so a scheduled // collector could miss a status sent before it starts. - rxBus.toFlow(EventDanaRNewStatus::class.java) + rxBus.toFlow(EventDanaRNewStatus::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { onPumpStatusUpdate() } - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { onPumpStatusUpdate() } } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt index 2f8f2d73369c..c1413c26057d 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/services/AbstractDanaRExecutionService.kt @@ -111,7 +111,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { // Service lifetime scope on IO, like the io scheduler used before, cancelled in onDestroy // like the CompositeDisposable was cleared. UNDISPATCHED because RxBus has no replay, so a // scheduled collector could miss an event sent before it starts. - rxBus.toFlow(EventBTChange::class.java) + rxBus.toFlow(EventBTChange::class) .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> if (event.state === EventBTChange.Change.DISCONNECT) { aapsLogger.debug(LTag.PUMP, "Device was disconnected " + event.deviceName) //Device was disconnected @@ -121,7 +121,7 @@ abstract class AbstractDanaRExecutionService : DaggerService() { } } } - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { aapsLogger.debug(LTag.PUMP, "EventAppExit received") mSerialIOThread?.disconnect("Application exit") diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt index aa7885a63927..116e763cbacf 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt @@ -102,7 +102,7 @@ class DanaRKoreanPlugin @Inject constructor( // Same scope as the preference observer above: IO, like the io scheduler used before, and // cancelled in onStop like the CompositeDisposable was cleared. UNDISPATCHED because RxBus // has no replay, so a scheduled collector could miss an exit sent before it starts. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt index b2195ffe17be..b5b164ca80d2 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt @@ -117,7 +117,7 @@ class DanaRv2Plugin @Inject constructor( // collector could miss an exit sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } super.onStart() } diff --git a/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt b/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt index 79a5783d28b7..921e3a59df9f 100644 --- a/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt +++ b/pump/danar/src/test/kotlin/app/aaps/pump/danar/compose/DanaRPairWizardViewModelTest.kt @@ -74,8 +74,8 @@ internal class DanaRPairWizardViewModelTest { // init -> reset() -> refreshBondedDevices() reads the transport synchronously. whenever(rfcommTransport.getBondedDevices()).thenReturn(emptyList()) // init collects these two streams on viewModelScope (Main = the test dispatcher below). - whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(statusFlow) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(initFlow) + whenever(rxBus.toFlow(EventDanaRNewStatus::class)).thenReturn(statusFlow) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(initFlow) whenever(rh.gs(anyInt())).thenReturn("device changed") sut = buildViewModel() diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt index 4586d2aa1637..b7d1bf2bd912 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt @@ -128,9 +128,9 @@ class DanaRSPlugin @Inject constructor( // collector could miss an event sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } - rxBus.toFlow(EventConfigBuilderChange::class.java) + rxBus.toFlow(EventConfigBuilderChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { danaPump.reset() } changePump() // load device name } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt index 805f49c9289e..c3531117ca72 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/services/DanaRSService.kt @@ -164,7 +164,7 @@ class DanaRSService : DaggerService() { // IO like the io scheduler used before, cancelled in onDestroy like the CompositeDisposable // was cleared. UNDISPATCHED because RxBus has no replay, so a scheduled collector could miss // an exit sent before it starts. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } } diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt index 0d57dee3119a..69aab8225e8a 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt @@ -89,10 +89,10 @@ internal class DanaRSOverviewViewModelTest { whenever(danaPump.temporaryBasalToString()).thenReturn("") // rx / persistence wiring touched at construction - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventDanaRNewStatus::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventDanaRNewStatus::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt index 3d975edcc876..b0f61f48867b 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt @@ -117,11 +117,11 @@ class DiaconnG8Plugin @Inject constructor( // collector could miss an event sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } - rxBus.toFlow(EventConfigBuilderChange::class.java) + rxBus.toFlow(EventConfigBuilderChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { diaconnG8Pump.reset() } - rxBus.toFlow(EventDiaconnG8DeviceChange::class.java) + rxBus.toFlow(EventDiaconnG8DeviceChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { pumpSync.connectNewPump() changePump() diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt index da66e8d299d3..4f8b965cea79 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModel.kt @@ -68,7 +68,7 @@ class DiaconnHistoryViewModel @Inject constructor( // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay, so a scheduled // collector could miss a status sent before it starts. - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> _uiState.update { it.copy(statusMessage = rh.gs(event.getStatus())) } } diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt index 18276cece2bc..20dc18ca8d33 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt @@ -95,9 +95,9 @@ class DiaconnOverviewViewModel @Inject constructor( // viewModelScope dies with the view model like the CompositeDisposable did. The bodies only // write a timestamp, so the dispatcher does not matter. UNDISPATCHED because RxBus has no // replay, so a scheduled collector could miss an event sent before it starts. - rxBus.toFlow(EventDiaconnG8NewStatus::class.java) + rxBus.toFlow(EventDiaconnG8NewStatus::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } persistenceLayer.observeChanges(EB::class.java) diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt index 70cb9ec4d689..9f0859f6cd96 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/service/DiaconnG8Service.kt @@ -131,7 +131,7 @@ class DiaconnG8Service : DaggerService() { // Same scope as the preference observer below: IO, like the io scheduler used before, and // cancelled in onDestroy like the CompositeDisposable was cleared. UNDISPATCHED because // RxBus has no replay, so a scheduled collector could miss an exit sent before it starts. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } preferences.observe(DiaconnIntKey.BolusSpeed).drop(1).onEach { diaconnG8Pump.bolusSpeed = preferences.get(DiaconnIntKey.BolusSpeed) diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt index 335bae1a9018..4420ac43304b 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnHistoryViewModelTest.kt @@ -57,7 +57,7 @@ internal class DiaconnHistoryViewModelTest { // init runs synchronously (not in a launch): builds the type list, subscribes to status events, // and loads records for the first type — every collaborator it touches must be stubbed. whenever(rh.gs(anyInt())).thenReturn("") - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.main).thenReturn(Schedulers.trampoline()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(dateUtil.now()).thenReturn(0L) diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt index 009b15ad33e9..1470ef206b3b 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt @@ -84,10 +84,10 @@ internal class DiaconnOverviewViewModelTest { whenever(diaconnG8Pump.lastBolusAmountFlow).thenReturn(MutableStateFlow(null)) // rx wiring touched at construction (PumpCommunicationStatus + init subscriptions) - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventDiaconnG8NewStatus::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventDiaconnG8NewStatus::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt index 096c7237e784..a67711f63ed0 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/ble/PatchManager.kt @@ -112,7 +112,7 @@ class PatchManager @Inject constructor( // App lifetime scope, matching the CompositeDisposable here which is never cleared. IO, like // observeOn(aapsSchedulers.io). UNDISPATCHED because RxBus has no replay, so a scheduled // collector could miss an alert sent before it starts. - rxBus.toFlow(EventPatchActivationNotComplete::class.java) + rxBus.toFlow(EventPatchActivationNotComplete::class) .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { notificationManager.post( id = NotificationId.EOFLOW_PATCH_ALERT, diff --git a/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt b/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt index 766728f848cc..fef21ac0eebb 100644 --- a/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt +++ b/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt @@ -91,8 +91,8 @@ internal class EopatchOverviewViewModelTest { whenever(patchManagerExecutor.observePatchConnectionState()).thenReturn(Observable.empty()) // PumpCommunicationStatus field-initializer subscribes to these rx flows at construction. - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) // buildUiState computes insulinText unconditionally (before the isActivated guard). whenever(ch.insulinAmountString(any())).thenReturn("0 U") diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index f50b10a9db8c..5af576bffc9a 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -125,10 +125,10 @@ class EquilPumpPlugin @Inject constructor( val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventEquilDataChanged::class.java) + rxBus.toFlow(EventEquilDataChanged::class) .collectResilient(newScope, aapsLogger, LTag.PUMP) { playAlarm() } - rxBus.toFlow(EventEquilAlarm::class.java) + rxBus.toFlow(EventEquilAlarm::class) .collectResilient(newScope, aapsLogger, LTag.PUMP) { eventEquilError -> aapsLogger.info(LTag.PUMPCOMM, "eventEquilError.tips====${eventEquilError.tips}") // Always surface the pump alarm - it is no longer gated on a bolus being in progress diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModel.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModel.kt index 6ded202f0d64..27b847b1f0ec 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModel.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModel.kt @@ -68,7 +68,7 @@ class EquilHistoryViewModel @Inject constructor( init { loadData() - rxBus.toFlow(EventEquilDataChanged::class.java) + rxBus.toFlow(EventEquilDataChanged::class) .onEach { loadData() } .launchIn(viewModelScope) } diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt index 5df9b491824b..ee93c544adad 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModel.kt @@ -84,10 +84,10 @@ class EquilOverviewViewModel @Inject constructor( private val _refreshTrigger = MutableStateFlow(0L) init { - rxBus.toFlow(EventEquilDataChanged::class.java) + rxBus.toFlow(EventEquilDataChanged::class) .onEach { _refreshTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) - rxBus.toFlow(EventEquilModeChanged::class.java) + rxBus.toFlow(EventEquilModeChanged::class) .onEach { _refreshTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) } diff --git a/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModelTest.kt b/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModelTest.kt index 95a785e3fd52..f514ab7fa8bf 100644 --- a/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModelTest.kt +++ b/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilHistoryViewModelTest.kt @@ -55,7 +55,7 @@ internal class EquilHistoryViewModelTest { Dispatchers.setMain(StandardTestDispatcher()) // init evaluates rxBus.toFlow(...) synchronously (before the deferred launchIn) -> must stub - whenever(rxBus.toFlow(EventEquilDataChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventEquilDataChanged::class)).thenReturn(emptyFlow()) sut = EquilHistoryViewModel( equilHistoryRecordDao, diff --git a/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModelTest.kt b/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModelTest.kt index 73d000487f9e..e513ba62d3fe 100644 --- a/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModelTest.kt +++ b/pump/equil/src/test/kotlin/app/aaps/pump/equil/compose/EquilOverviewViewModelTest.kt @@ -65,10 +65,10 @@ internal class EquilOverviewViewModelTest { // init { } subscribes to these via launchIn(viewModelScope), and PumpCommunicationStatus // subscribes to the two core events at construction. Unstubbed -> construction NPEs. - whenever(rxBus.toFlow(EventEquilDataChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventEquilModeChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventEquilDataChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventEquilModeChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) // formatting collaborators used by buildPairedInfoRows / buildStatusBanner whenever(ch.insulinAmountString(any())).thenReturn("0 U") diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt index a4195c946d32..ef0c8de4d5be 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightComposeContent.kt @@ -278,7 +278,7 @@ internal class InsightOverviewState( // sent before it starts. val newScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) scope = newScope - rxBus.toFlow(EventLocalInsightUpdateGUI::class.java) + rxBus.toFlow(EventLocalInsightUpdateGUI::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { refresh() } refresh() } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index c698135ecafb..f6eca750c452 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -213,7 +213,7 @@ class MedtronicPumpPlugin @Inject constructor( val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope // Pass only to setup wizard - rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) } diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt index 194bf52a1969..25550760efe9 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt @@ -99,15 +99,15 @@ class MedtronicOverviewViewModel @Inject constructor( private val medtronicRefresh = MutableStateFlow(0L).also { flow -> viewModelScope.launch { - rxBus.toFlow(EventMedtronicPumpValuesChanged::class.java) + rxBus.toFlow(EventMedtronicPumpValuesChanged::class) .collect { flow.value = System.currentTimeMillis() } } viewModelScope.launch { - rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class) .collect { flow.value = System.currentTimeMillis() } } viewModelScope.launch { - rxBus.toFlow(EventMedtronicPumpConfigurationChanged::class.java) + rxBus.toFlow(EventMedtronicPumpConfigurationChanged::class) .collect { aapsLogger.debug(LTag.PUMP, "EventMedtronicPumpConfigurationChanged triggered") medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() diff --git a/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModelTest.kt b/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModelTest.kt index e7c315073003..56d70327a06f 100644 --- a/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModelTest.kt +++ b/pump/medtronic/src/test/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModelTest.kt @@ -82,11 +82,11 @@ internal class MedtronicOverviewViewModelTest { // rx wiring touched at construction: PumpCommunicationStatus init + the three medtronicRefresh // collectors launched in viewModelScope (UnconfinedTestDispatcher runs them eagerly). - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventMedtronicPumpValuesChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventMedtronicPumpConfigurationChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventMedtronicPumpValuesChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventMedtronicPumpConfigurationChanged::class)).thenReturn(emptyFlow()) // Pump state read by buildUiState() -> buildInfoRows() (kept minimal to skip optional branches). whenever(rileyLinkServiceData.rileyLinkServiceState).thenReturn(RileyLinkServiceState.NotStarted) diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index 8650ef8e6d4f..768d0f9b295b 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -112,7 +112,7 @@ class MedtrumPlugin @Inject constructor( // has no replay, so a scheduled collector could miss an exit sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { context.unbindService(mConnection) } preferences.observe(MedtrumStringNonKey.SnInput).drop(1).collectResilient(newScope, aapsLogger, LTag.PUMP) { updateMaxInsulinLimitsForPumpType() diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt index d1285fad5276..5be125f06174 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/services/MedtrumService.kt @@ -129,7 +129,7 @@ class MedtrumService : DaggerService(), MedtrumBleCallback { // Same service scope as the preference observers below, which is IO like the io scheduler // used before. UNDISPATCHED because RxBus has no replay, so a scheduled collector could miss // an exit sent before it starts. - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(scope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { stopSelf() } preferences.observe(MedtrumStringNonKey.SnInput).drop(1).collectResilient(scope, aapsLogger, LTag.PUMP) { aapsLogger.debug(LTag.PUMPCOMM, "Serial number changed, reporting new pump!") diff --git a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt index 43b4dfbb07dc..aab5322bb346 100644 --- a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt +++ b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt @@ -85,8 +85,8 @@ internal class MedtrumOverviewViewModelTest { whenever(medtrumPump.activeAlarms).thenReturn(EnumSet.noneOf(AlarmState::class.java)) // PumpCommunicationStatus init subscribes to these two flows at construction - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) // Formatting collaborator (base basal rate row is always built) whenever(ch.basalRateString(any(), any(), any())).thenReturn("0.00 U/h") diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt index fcf5d939cd45..cba4f4debe20 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt @@ -115,7 +115,7 @@ class DashOverviewViewModel @Inject constructor( // Trigger flow from RxBus omnipod events private val omnipodRefresh = MutableStateFlow(0L).also { flow -> scope.launch { - rxBus.toFlow(EventOmnipodDashPumpValuesChanged::class.java) + rxBus.toFlow(EventOmnipodDashPumpValuesChanged::class) .collect { flow.value = System.currentTimeMillis() } } } diff --git a/pump/omnipod/dash/src/test/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModelTest.kt b/pump/omnipod/dash/src/test/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModelTest.kt index 7eb10f9a382d..eb7aafcd0819 100644 --- a/pump/omnipod/dash/src/test/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModelTest.kt +++ b/pump/omnipod/dash/src/test/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModelTest.kt @@ -74,9 +74,9 @@ internal class DashOverviewViewModelTest { // rx wiring touched at construction: PumpCommunicationStatus subscribes to these two, and the // omnipodRefresh field launches a collector on EventOmnipodDashPumpValuesChanged. - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventOmnipodDashPumpValuesChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventOmnipodDashPumpValuesChanged::class)).thenReturn(emptyFlow()) // buildUiState() reads activationProgress heavily; NOT_STARTED keeps the simple pre-activation branch. whenever(podStateManager.activationProgress).thenReturn(ActivationProgress.NOT_STARTED) diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index bb0f1a483358..3b98f0e25e66 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -259,18 +259,18 @@ class OmnipodErosPumpPlugin @Inject constructor( // has no replay, so a scheduled collector could miss an event sent before it starts. val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = newScope - rxBus.toFlow(EventAppExit::class.java) + rxBus.toFlow(EventAppExit::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { serviceConnection?.let { context.unbindService(it) } } - rxBus.toFlow(EventOmnipodErosTbrChanged::class.java) + rxBus.toFlow(EventOmnipodErosTbrChanged::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleCancelledTbr() } - rxBus.toFlow(EventOmnipodErosUncertainTbrRecovered::class.java) + rxBus.toFlow(EventOmnipodErosUncertainTbrRecovered::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleUncertainTbrRecovery() } - rxBus.toFlow(EventOmnipodErosActiveAlertsChanged::class.java) + rxBus.toFlow(EventOmnipodErosActiveAlertsChanged::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handleActivePodAlerts() } - rxBus.toFlow(EventOmnipodErosFaultEventChanged::class.java) + rxBus.toFlow(EventOmnipodErosFaultEventChanged::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { handlePodFaultEvent() } // Pass only to setup wizard - rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { event -> rxBus.send(EventSWRLStatus(rh.gs(event.getStatus()))) } @@ -300,7 +300,7 @@ class OmnipodErosPumpPlugin @Inject constructor( commandQueue.customCommand(CommandUpdateAlertConfiguration()) } } - rxBus.toFlow(EventAppInitialized::class.java) + rxBus.toFlow(EventAppInitialized::class) .collectResilient(newScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { // See if a bolus was active before the app previously exited // If so, add it to history diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt index d11c3bf8af1b..64d10110a56c 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt @@ -131,11 +131,11 @@ class ErosOverviewViewModel @Inject constructor( private val omnipodRefresh = MutableStateFlow(0L).also { flow -> scope.launch { - rxBus.toFlow(EventOmnipodErosPumpValuesChanged::class.java) + rxBus.toFlow(EventOmnipodErosPumpValuesChanged::class) .collect { flow.value = System.currentTimeMillis() } } scope.launch { - rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class) .collect { flow.value = System.currentTimeMillis() } } } diff --git a/pump/omnipod/eros/src/test/kotlin/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModelTest.kt b/pump/omnipod/eros/src/test/kotlin/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModelTest.kt index 4cd35c3f7e11..fb99ee12d8f5 100644 --- a/pump/omnipod/eros/src/test/kotlin/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModelTest.kt +++ b/pump/omnipod/eros/src/test/kotlin/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModelTest.kt @@ -88,10 +88,10 @@ internal class ErosOverviewViewModelTest { Dispatchers.setMain(UnconfinedTestDispatcher()) // PumpCommunicationStatus + omnipodRefresh subscribe to these at construction. - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventOmnipodErosPumpValuesChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventOmnipodErosPumpValuesChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class)).thenReturn(emptyFlow()) // buildInfoRows()/buildManagementActions() read these unconditionally at construction. whenever(rileyLinkServiceData.rileyLinkServiceState).thenReturn(RileyLinkServiceState.NotStarted) diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModel.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModel.kt index 324fbe99078c..a5ed966ecec3 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModel.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModel.kt @@ -67,7 +67,7 @@ class RileyLinkStatusViewModel @Inject constructor( init { refresh() viewModelScope.launch { - rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class) .collect { refresh() } } } diff --git a/pump/rileylink/src/test/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModelTest.kt b/pump/rileylink/src/test/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModelTest.kt index 7f4cd0200cad..14e2a8f5aa7f 100644 --- a/pump/rileylink/src/test/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModelTest.kt +++ b/pump/rileylink/src/test/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModelTest.kt @@ -58,7 +58,7 @@ internal class RileyLinkStatusViewModelTest { // buildState() reads activePumpInternal first; a non-RileyLink pump makes it return default state. whenever(activePlugin.activePumpInternal).thenReturn(activePump) // Defensive: the deferred collector body calls rxBus.toFlow(...) before .collect { } if ever run. - whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRileyLinkDeviceStatusChange::class)).thenReturn(emptyFlow()) sut = RileyLinkStatusViewModel( rh, rileyLinkServiceData, rileyLinkUtil, activePlugin, dateUtil, preferences, rxBus diff --git a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt index 2330918fcb9b..4a5f16aae0c0 100644 --- a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt +++ b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt @@ -49,8 +49,8 @@ internal class VirtualPumpViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) // rxBus flows consumed by PumpCommunicationStatus init - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(pumpStatusFlow) - whenever(rxBus.toFlow(EventQueueChanged::class.java)).thenReturn(queueChangedFlow) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(pumpStatusFlow) + whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(queueChangedFlow) // DB change flows merged into dbChanged (evaluated eagerly in the constructor) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt index 1ee04d713de0..7ef797b22e32 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/rx/bus/RxBusImpl.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlin.reflect.KClass import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +31,7 @@ class RxBusImpl @Inject constructor( } @Suppress("UNCHECKED_CAST") - override fun toFlow(eventType: Class): Flow = + override fun toFlow(eventType: KClass): Flow = flowPublisher .filter { eventType.isInstance(it) } .map { it as T } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModel.kt index e5e9adaafff5..685fbf4a3e46 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModel.kt @@ -46,13 +46,13 @@ class LoopActionViewModel @Inject constructor( val uiState: StateFlow = _uiState.asStateFlow() init { - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventNewOpenLoopNotification::class.java) + rxBus.toFlow(EventNewOpenLoopNotification::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventRefreshOverview::class.java) + rxBus.toFlow(EventRefreshOverview::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventAcceptOpenLoopChange::class.java) + rxBus.toFlow(EventAcceptOpenLoopChange::class) .onEach { refreshState() }.launchIn(viewModelScope) refreshState() } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt index da402f448348..c26a2b860a92 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt @@ -94,13 +94,13 @@ class ManageViewModel @Inject constructor( } private fun setupEventListeners() { - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) persistenceLayer.observeChanges(EB::class.java) .onEach { refreshState() }.launchIn(viewModelScope) persistenceLayer.observeChanges(TB::class.java) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventCustomActionsChanged::class.java) + rxBus.toFlow(EventCustomActionsChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) // Re-evaluate showMutatingActions when the client pairs/unpairs (stable signal, flips rarely). nsClient.masterOrPairedClientFlow diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt index 30f16961e31d..ad99c2380570 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt @@ -310,7 +310,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // Refresh trend arrow after bucketed data is created (bucketed data is ready after this event) scope.launch { - rxBus.toFlow(EventBucketedDataCreated::class.java).collect { + rxBus.toFlow(EventBucketedDataCreated::class).collect { aapsLogger.debug(LTag.UI, "Bucketed data created, refreshing BgInfo for trend arrow") updateBgInfoFromDatabase() } @@ -399,8 +399,8 @@ class OverviewDataCacheImpl @AssistedInject constructor( // loop.lastRun.constraintsProcessed.targetBG) is reflected in the ADJUSTED state. scope.launch { merge( - rxBus.toFlow(EventLoopUpdateGui::class.java), - rxBus.toFlow(EventNewOpenLoopNotification::class.java) + rxBus.toFlow(EventLoopUpdateGui::class), + rxBus.toFlow(EventNewOpenLoopNotification::class) ).collect { updateTempTargetFromDatabase() } } // AAPSCLIENT counterpart: the ADJUSTED text comes from @@ -411,7 +411,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // APS result no longer matches the just-expired TT. if (config.AAPSCLIENT) { scope.launch { - rxBus.toFlow(EventNsClientStatusUpdated::class.java) + rxBus.toFlow(EventNsClientStatusUpdated::class) .debounce(300) .collect { updateTempTargetFromDatabase() } } @@ -477,7 +477,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( if (!hasSubscribers) return@collectLatest rebuildNsClientStatus() launch { - rxBus.toFlow(EventNsClientStatusUpdated::class.java).collect { + rxBus.toFlow(EventNsClientStatusUpdated::class).collect { rebuildNsClientStatus() } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt index 326faf3421f2..574511ef64fa 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt @@ -67,15 +67,15 @@ class StatusViewModel @Inject constructor( } private fun setupEventListeners() { - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) persistenceLayer.observeChanges(TE::class.java) .onEach { refreshState() }.launchIn(viewModelScope) persistenceLayer.databaseClearedFlow .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventNsClientStatusUpdated::class.java) + rxBus.toFlow(EventNsClientStatusUpdated::class) .onEach { refreshState() }.launchIn(viewModelScope) tickerFlow(60_000L) .onEach { refreshState() }.launchIn(viewModelScope) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt index 677206a1764f..d0ae4971ed00 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModel.kt @@ -125,7 +125,7 @@ class QuickWizardManagementViewModel @Inject constructor( // viewModelScope is Main, like observeOn(aapsSchedulers.main), and dies with the view model // like the CompositeDisposable did. UNDISPATCHED because RxBus has no replay: a scheduled // collector could miss a change sent before it starts. - rxBus.toFlow(EventQuickWizardChange::class.java) + rxBus.toFlow(EventQuickWizardChange::class) .collectResilient(viewModelScope, aapsLogger, LTag.UI, start = CoroutineStart.UNDISPATCHED) { loadData() } } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt index 033194609211..24bdf3cfe3a7 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenes/SceneListViewModel.kt @@ -125,13 +125,13 @@ class SceneListViewModel @Inject constructor( } } // Refresh activationReasons on relevant runtime-state events. - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .onEach { activationTick.update { it + 1 } }.launchIn(viewModelScope) - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .onEach { activationTick.update { it + 1 } }.launchIn(viewModelScope) - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .onEach { activationTick.update { it + 1 } }.launchIn(viewModelScope) - rxBus.toFlow(EventRefreshOverview::class.java) + rxBus.toFlow(EventRefreshOverview::class) .onEach { activationTick.update { it + 1 } }.launchIn(viewModelScope) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModel.kt index 1801b1639c4f..cd0b57909e5e 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModel.kt @@ -89,7 +89,7 @@ class ScenesViewModel @Inject constructor( private fun setupEventListeners() { // RxBus flows are hot and don't replay, so initial subscription doesn't refresh — // init { refreshState() } below covers the cold start. - rxBus.toFlow(EventRefreshOverview::class.java) + rxBus.toFlow(EventRefreshOverview::class) .onEach { refreshState() }.launchIn(viewModelScope) // StateFlow — drop(1) since init{} already reads current automation events; only react to changes. automation.events.drop(1) @@ -98,11 +98,11 @@ class ScenesViewModel @Inject constructor( // Without these, transient "no profile / pump disconnected" windows // would wipe automation items and never restore them until another // event (e.g. editing a scene) re-fired refreshState. - rxBus.toFlow(EventPumpStatusChanged::class.java) + rxBus.toFlow(EventPumpStatusChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventLoopUpdateGui::class.java) + rxBus.toFlow(EventLoopUpdateGui::class) .onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventInitializationChanged::class.java) + rxBus.toFlow(EventInitializationChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) // StateFlow — drop(1) since init{} already reads current scenes; only react to changes. sceneRepository.scenesFlow diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModel.kt index b06668ac5e10..2863c419ccf2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModel.kt @@ -81,7 +81,7 @@ class TreatmentViewModel @Inject constructor( // QuickWizard entries changed (local edit or synced from the main phone). quickWizard.changes.drop(1).map {}, ).onEach { refreshState() }.launchIn(viewModelScope) - rxBus.toFlow(EventRefreshOverview::class.java) + rxBus.toFlow(EventRefreshOverview::class) .onEach { refreshState() }.launchIn(viewModelScope) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModelTest.kt index e99acbd8dc60..c64424153be7 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/loopSheet/LoopActionViewModelTest.kt @@ -40,10 +40,10 @@ internal class LoopActionViewModelTest { // The init observer launchIns + the refreshState() coroutine are deferred by StandardTestDispatcher; // the observed rxBus flows are still built synchronously in init, so they must be non-null. Dispatchers.setMain(StandardTestDispatcher()) - whenever(rxBus.toFlow(EventLoopUpdateGui::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventNewOpenLoopNotification::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventRefreshOverview::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventAcceptOpenLoopChange::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventLoopUpdateGui::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventNewOpenLoopNotification::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRefreshOverview::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventAcceptOpenLoopChange::class)).thenReturn(emptyFlow()) sut = LoopActionViewModel(loop, activePlugin, profileFunction, rh, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt index fd4fe5d7e359..6196c4c512e9 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt @@ -76,8 +76,8 @@ internal class ManageViewModelTest { // setupEventListeners() builds cold flow chains synchronously in init — every source must be non-null. whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventCustomActionsChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventCustomActionsChanged::class)).thenReturn(emptyFlow()) whenever(nsClient.masterOrPairedClientFlow).thenReturn(MutableStateFlow(false)) // Field init casts activePumpInternal (a Pump) to PluginBase — the fake satisfies both. pumpPlugin = mock() diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt index cb390dd1dd8a..1572302ca56e 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt @@ -53,9 +53,9 @@ internal class StatusViewModelTest { // so construction stays clean and we test the default uiState. The event-listener flows are still // built synchronously in init, so they must be non-null. Dispatchers.setMain(StandardTestDispatcher()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventNsClientStatusUpdated::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventNsClientStatusUpdated::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) whenever(persistenceLayer.databaseClearedFlow).thenReturn(emptyFlow()) sut = StatusViewModel( diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt index 12739de198ad..6e3f7f98e39a 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/quickWizard/viewmodels/QuickWizardManagementViewModelTest.kt @@ -55,7 +55,7 @@ internal class QuickWizardManagementViewModelTest { Dispatchers.setMain(StandardTestDispatcher()) // Synchronous init wiring that must return non-null flows/streams to avoid construction NPEs. whenever(quickWizard.changes).thenReturn(MutableStateFlow(0)) - whenever(rxBus.toFlow(EventQuickWizardChange::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventQuickWizardChange::class)).thenReturn(emptyFlow()) sut = QuickWizardManagementViewModel( quickWizard, rxBus, constraintChecker, preferences, rh, dateUtil, aapsLogger ) diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/scenes/SceneListViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/scenes/SceneListViewModelTest.kt index 787387feb8c8..51e6222528d2 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/scenes/SceneListViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/scenes/SceneListViewModelTest.kt @@ -64,10 +64,10 @@ internal class SceneListViewModelTest { whenever(activeSceneManager.activeSceneState).thenReturn(MutableStateFlow(null)) whenever(nsClient.masterReachable).thenReturn(MutableStateFlow(false)) // init references these event flows synchronously (onEach before the deferred launchIn): - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventLoopUpdateGui::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventRefreshOverview::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventLoopUpdateGui::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRefreshOverview::class)).thenReturn(emptyFlow()) sut = SceneListViewModel( sceneRepository, activeSceneManager, persistenceLayer, profileRepository, rh, rxBus, dateUtil, config, sceneActions, sceneChainTargetResolver, nsClient diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModelTest.kt index 72123b2013ee..a3902185849f 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/scenesSheet/ScenesViewModelTest.kt @@ -56,10 +56,10 @@ internal class ScenesViewModelTest { whenever(nsClient.masterReachable).thenReturn(MutableStateFlow(true)) whenever(automation.events).thenReturn(MutableStateFlow>(emptyList())) whenever(sceneRepository.scenesFlow).thenReturn(MutableStateFlow("")) - whenever(rxBus.toFlow(EventRefreshOverview::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventPumpStatusChanged::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventLoopUpdateGui::class.java)).thenReturn(emptyFlow()) - whenever(rxBus.toFlow(EventInitializationChanged::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRefreshOverview::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventLoopUpdateGui::class)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) sut = ScenesViewModel( automation, activePlugin, loop, profileFunction, config, rxBus, sceneRepository, sceneActions, rh, nsClient diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModelTest.kt index b2b4baf42116..2dca65743a57 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatmentsSheet/TreatmentViewModelTest.kt @@ -65,7 +65,7 @@ internal class TreatmentViewModelTest { whenever(preferences.observe(BooleanKey.OverviewShowWizardButton)).thenReturn(MutableStateFlow(false)) whenever(preferences.observe(BooleanKey.GeneralSimpleMode)).thenReturn(MutableStateFlow(false)) whenever(quickWizard.changes).thenReturn(MutableStateFlow(0)) - whenever(rxBus.toFlow(EventRefreshOverview::class.java)).thenReturn(emptyFlow()) + whenever(rxBus.toFlow(EventRefreshOverview::class)).thenReturn(emptyFlow()) sut = TreatmentViewModel( rh, preferences, activePlugin, config, profileFunction, loop, iobCobCalculator, constraintChecker, quickWizard, rxBus, aapsLogger, dexcomBoyda, elementAvailability diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt index d5b9915d8dc4..632b5e2e297b 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt @@ -103,7 +103,7 @@ class DataHandlerWear @Inject constructor( // dataStoreScope is Dispatchers.IO, matching observeOn(aapsSchedulers.io). UNDISPATCHED because // these subscribe from setupBus() on a replay-0 bus: a scheduled collector could miss anything // sent before it started. - rxBus.toFlow(T::class.java) + rxBus.toFlow(T::class) .collectResilient(dataStoreScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> aapsLogger.debug(LTag.WEAR, "${T::class.java.simpleName} received from ${event.sourceNodeId}${detail(event)}") handler(event) diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 8104caefb73d..99e431e0589b 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -78,15 +78,15 @@ class DataLayerListenerServiceWear : WearableListenerService() { // scope is Main.immediate, which is the right lifetime. The two sends observed on io and talk // to the Data Layer, so those bodies go back to IO; the preference one observed on main and // touches the listeners, so it stays where the collector is. - rxBus.toFlow(EventWearToMobile::class.java) + rxBus.toFlow(EventWearToMobile::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { withContext(Dispatchers.IO) { sendMessage(rxPath, it.payload.serialize()) } } - rxBus.toFlow(EventWearDataToMobile::class.java) + rxBus.toFlow(EventWearDataToMobile::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { withContext(Dispatchers.IO) { sendMessage(rxDataPath, it.payload.serializeByte()) } } - rxBus.toFlow(EventWearPreferenceChange::class.java) + rxBus.toFlow(EventWearPreferenceChange::class) .collectResilient(scope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> if (event.changedKey == getString(R.string.key_heart_rate_sampling)) updateHeartRateListener() if (event.changedKey == getString(R.string.key_steps_sampling)) updateStepsCountListener() diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt index 5853ef3ddf34..94a19cea3678 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/activities/LoopStatusActivity.kt @@ -140,7 +140,7 @@ class LoopStatusActivity : AppCompatActivity() { // lifecycleScope is Main and dies with the activity, so runOnUiThread is no longer needed. // The Rx onError put the screen into an error state rather than only logging, so that is kept // explicitly - collectResilient on its own would log and carry on with the UI still spinning. - rxBus.toFlow(EventData.LoopStatusResponse::class.java) + rxBus.toFlow(EventData.LoopStatusResponse::class) .collectResilient(lifecycleScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { event -> try { aapsLogger.debug(LTag.WEAR, "Received loop status response") diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt index c3dba7b7f6ce..6bb91e106576 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/MenuListActivity.kt @@ -70,7 +70,7 @@ abstract class MenuListActivity : DaggerAppCompatActivity() { super.onCreate(savedInstanceState) // lifecycleScope is Main, which is what observeOn(aapsSchedulers.main) supplied, and it dies // with the activity like the CompositeDisposable did. - rxBus.toFlow(EventUpdateSelectedWatchface::class.java) + rxBus.toFlow(EventUpdateSelectedWatchface::class) .collectResilient(lifecycleScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { elements = provideElements() } elements = provideElements() val menuTitle = title.toString() diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt index 620afd417235..cea2b4fa79c6 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt @@ -124,7 +124,7 @@ class CircleWatchface : WatchFace() { } // watchfaceScope is Main.immediate, matching observeOn(aapsSchedulers.main). - rxBus.toFlow(EventData.Preferences::class.java) + rxBus.toFlow(EventData.Preferences::class) .collectResilient(watchfaceScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { if (myLayout != null) { // Only update if layout initialized prepareDrawTime() diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt index 88ef3785f12b..1ad64b04ace7 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt @@ -176,7 +176,7 @@ abstract class BaseWatchFace : WatchFace() { specW = View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY) specH = if (forceSquareCanvas) specW else View.MeasureSpec.makeMeasureSpec(displayHeight, View.MeasureSpec.EXACTLY) // watchfaceScope is Main.immediate, matching observeOn(aapsSchedulers.main). - rxBus.toFlow(EventWearPreferenceChange::class.java) + rxBus.toFlow(EventWearPreferenceChange::class) .collectResilient(watchfaceScope, aapsLogger, LTag.WEAR, start = CoroutineStart.UNDISPATCHED) { simpleUi.updatePreferences() if (::binding.isInitialized && layoutSet) setDataFields() diff --git a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt index d3cc36ddc2c5..5712bf2e73b2 100644 --- a/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt +++ b/wear/src/test/kotlin/app/aaps/wear/comm/DataHandlerWearTest.kt @@ -69,7 +69,7 @@ internal class DataHandlerWearTest : WearTestBase() { val pong = CompletableDeferred() // UNDISPATCHED so the collector is subscribed before the ping is sent; RxBus has no replay. val collector = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { - rxBus.toFlow(EventWearToMobile::class.java).collect { evt -> + rxBus.toFlow(EventWearToMobile::class).collect { evt -> (evt.payload as? EventData.ActionPong)?.let { pong.complete(it) } } } From 687af15829dc94493306e1873ff29424f23601b6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 13:12:24 +0200 Subject: [PATCH 109/146] PersistenceLayer.observeChanges keys on KClass Same change as RxBus, and it finishes something that was already half done here: observeAnyChange has always returned Flow>>, and a reified observeChanges() extension already existed, it just unwrapped to Class internally. 89 call sites drop .java. The when in PersistenceLayerImpl matches on KClass now, the test stubs match any> instead of any>, and one list of types in OverviewDataCacheImpl needed converting by hand because it is built with listOf rather than passed inline. KClass.simpleName is nullable where Class.simpleName is not, which the default argument of awaitDbChange in androidTest relied on. This moves PersistenceLayer to commonMain and nothing else: everything that references it is held up by something else, mostly ResourceHelper. 205 files in commonMain now, 55 left. --- .../kotlin/app/aaps/CobExtendedCarbsTest.kt | 14 ++++---- .../app/aaps/helpers/IntegrationWaits.kt | 5 +-- .../core/interfaces/db/PersistenceLayer.kt | 4 +-- .../persistence/PersistenceLayerImpl.kt | 36 +++++++++---------- .../profile/ProfileFunctionImpl.kt | 2 +- .../profile/ProfileSwitchExpiryScheduler.kt | 2 +- .../queue/CommandQueueImplementation.kt | 2 +- .../insulin/InsulinImplMigrationTest.kt | 3 +- .../insulin/InsulinImplSyncTest.kt | 3 +- .../implementation/insulin/InsulinImplTest.kt | 3 +- .../profile/ProfileFunctionImplTest.kt | 18 +++++----- .../ProfileSwitchExpirySchedulerTest.kt | 5 +-- .../queue/CommandQueueImplementationTest.kt | 3 +- .../scenes/ActiveSceneManagerTest.kt | 8 ++--- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 2 +- .../runningMode/RunningModeExpiryScheduler.kt | 2 +- .../loop/runningMode/RunningModeReconciler.kt | 2 +- .../RunningModeExpirySchedulerTest.kt | 7 ++-- .../runningMode/RunningModeReconcilerTest.kt | 11 +++--- .../compose/CalibrationViewModelTest.kt | 4 +-- .../IobCobCalculatorPlugin.kt | 14 ++++---- .../smoothing/UnscentedKalmanFilterPlugin.kt | 2 +- .../source/compose/BgSourceViewModelTest.kt | 2 +- .../aaps/plugins/sync/garmin/GarminPlugin.kt | 2 +- .../plugins/sync/tidepool/TidepoolPlugin.kt | 2 +- .../sync/nsclientV3/NSClientV3PluginTest.kt | 3 +- .../nsclientV3/workers/LoadBgWorkerTest.kt | 3 +- .../workers/LoadDeviceStatusWorkerTest.kt | 3 +- .../nsclientV3/workers/LoadFoodsWorkerTest.kt | 3 +- .../workers/LoadLastModificationWorkerTest.kt | 3 +- .../workers/LoadProfileStoreWorkerTest.kt | 3 +- .../workers/LoadStatusWorkerTest.kt | 3 +- .../workers/LoadTreatmentsWorkerTest.kt | 3 +- .../sync/tidepool/TidepoolPluginTest.kt | 3 +- .../dana/compose/DanaOverviewViewModel.kt | 4 +-- .../dana/compose/DanaOverviewViewModelTest.kt | 4 +-- .../compose/DanaRSOverviewViewModelTest.kt | 4 +-- .../compose/DiaconnOverviewViewModel.kt | 4 +-- .../compose/DiaconnOverviewViewModelTest.kt | 4 +-- .../pump/virtual/VirtualPumpViewModelTest.kt | 6 ++-- .../ui/compose/manageSheet/ManageViewModel.kt | 4 +-- .../compose/overview/OverviewDataCacheImpl.kt | 18 +++++----- .../overview/statusLights/StatusViewModel.kt | 2 +- .../viewmodels/ProfileManagementViewModel.kt | 4 +-- .../SiteRotationManagementViewModel.kt | 2 +- .../FoodManagementViewModelTest.kt | 2 +- .../InsulinManagementViewModelTest.kt | 2 +- .../manageSheet/ManageViewModelTest.kt | 4 +-- .../statusLights/StatusViewModelTest.kt | 2 +- .../viewmodels/ProfileHelperViewModelTest.kt | 2 +- .../ProfileManagementViewModelTest.kt | 2 +- .../RunningModeManagementViewModelTest.kt | 4 +-- .../SiteRotationManagementViewModelTest.kt | 2 +- .../TempTargetManagementViewModelTest.kt | 2 +- .../viewmodels/BolusCarbsViewModelTest.kt | 6 ++-- .../viewmodels/CareportalViewModelTest.kt | 2 +- .../viewmodels/ExtendedBolusViewModelTest.kt | 2 +- .../viewmodels/ProfileSwitchViewModelTest.kt | 4 +-- .../viewmodels/RunningModeViewModelTest.kt | 2 +- .../viewmodels/TempBasalViewModelTest.kt | 2 +- .../viewmodels/TempTargetViewModelTest.kt | 2 +- .../viewmodels/UserEntryViewModelTest.kt | 2 +- 62 files changed, 151 insertions(+), 134 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt (99%) diff --git a/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt b/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt index 4c3a641f4aeb..86f6f412bb6d 100644 --- a/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt +++ b/app/src/androidTest/kotlin/app/aaps/CobExtendedCarbsTest.kt @@ -123,7 +123,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { // Create the profile switch and wait for the resulting EffectiveProfileSwitch (written by the // command queue once the pump push succeeds). Replaces old EventEffectiveProfileSwitchChanged. - val epsList = waits.awaitDbChange(EPS::class.java, what = "EffectiveProfileSwitch after createProfileSwitch") { + val epsList = waits.awaitDbChange(EPS::class, what = "EffectiveProfileSwitch after createProfileSwitch") { val result = profileFunction.createProfileSwitch( profileStore = store, profileName = profileName, @@ -188,7 +188,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { rxHelper.resetState(EventAutosensCalculationFinished::class) // Insert BG and wait for the GV flow emission (replaces old EventNewBG) - val gvList = waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + val gvList = waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertFlatBgData(now, 60, 100.0) } aapsLogger.info(LTag.CORE, "GV flow emitted ${gvList.size} entries") @@ -401,7 +401,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { val now = dateUtil.now() rxHelper.resetState(EventAutosensCalculationFinished::class) - waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertFlatBgData(now, 240, 100.0) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 15 * 60_000L) @@ -418,7 +418,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { val now = dateUtil.now() rxHelper.resetState(EventAutosensCalculationFinished::class) - waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertFlatBgData(now, 240, 100.0) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 0) @@ -437,7 +437,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { val now = dateUtil.now() rxHelper.resetState(EventAutosensCalculationFinished::class) - waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertBgData(now, 60, { minutesAgo -> 200.0 - minutesAgo * (100.0 / 60.0) }, TrendArrow.FORTY_FIVE_UP) } insertCarbs(now - 30 * 60_000L, 35.0, 2 * 60 * 60_000L) @@ -453,7 +453,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { val now = dateUtil.now() rxHelper.resetState(EventAutosensCalculationFinished::class) - waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertBgData(now, 60, { minutesAgo -> 180.0 - minutesAgo * (100.0 / 60.0) }, TrendArrow.FORTY_FIVE_UP) } insertCarbs(now - 20 * 60_000L, 20.0, 0) @@ -469,7 +469,7 @@ class CobExtendedCarbsTest : HiltInstrumentedTest() { val now = dateUtil.now() rxHelper.resetState(EventAutosensCalculationFinished::class) - waits.awaitDbChange(GV::class.java, what = "GlucoseValue after BG insert") { + waits.awaitDbChange(GV::class, what = "GlucoseValue after BG insert") { insertBgData(now, 240, { minutesAgo -> 250.0 - minutesAgo * (170.0 / 240.0) }, TrendArrow.FORTY_FIVE_UP) } insertCarbs(now - 4 * 60 * 60_000L, 10.0, 15 * 60_000L) diff --git a/app/src/androidTest/kotlin/app/aaps/helpers/IntegrationWaits.kt b/app/src/androidTest/kotlin/app/aaps/helpers/IntegrationWaits.kt index fc1905e60aeb..52c235a6b0ca 100644 --- a/app/src/androidTest/kotlin/app/aaps/helpers/IntegrationWaits.kt +++ b/app/src/androidTest/kotlin/app/aaps/helpers/IntegrationWaits.kt @@ -1,5 +1,6 @@ package app.aaps.helpers +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.logging.AAPSLogger @@ -38,8 +39,8 @@ class IntegrationWaits @Inject constructor( * On timeout this fails with a message naming [what] instead of an opaque coroutine timeout. */ suspend fun awaitDbChange( - type: Class, - what: String = type.simpleName, + type: KClass, + what: String = type.simpleName ?: "?", timeoutMs: Long = 40_000, action: suspend () -> Unit ): List = coroutineScope { diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt similarity index 99% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt index 7ba152c5bbb8..3cec74dd81c2 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/db/PersistenceLayer.kt @@ -88,7 +88,7 @@ interface PersistenceLayer { * @param T The domain type to observe (BS, CA, EB, TB, TT, TE, PS, EPS, etc.) * @return Flow that emits list of changed entities of type T */ - fun observeChanges(type: Class): Flow> + fun observeChanges(type: KClass): Flow> /** * Observe all database changes, emitting the set of domain types that changed in each transaction @@ -1649,4 +1649,4 @@ interface PersistenceLayer { * ``` */ inline fun PersistenceLayer.observeChanges(): Flow> = - observeChanges(T::class.java) \ No newline at end of file + observeChanges(T::class) \ No newline at end of file diff --git a/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt b/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt index 22b1a4862f6a..74c39af6e714 100644 --- a/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt +++ b/database/persistence/src/main/kotlin/app/aaps/database/persistence/PersistenceLayerImpl.kt @@ -179,58 +179,58 @@ class PersistenceLayerImpl @Inject constructor( // Flow-based change observation @Suppress("UNCHECKED_CAST") - override fun observeChanges(type: Class): Flow> { + override fun observeChanges(type: KClass): Flow> { // Map database entity changes to domain types return when (type) { - BS::class.java -> repository.changesOfType() + BS::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - CA::class.java -> repository.changesOfType() + CA::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - BCR::class.java -> repository.changesOfType() + BCR::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - EB::class.java -> repository.changesOfType() + EB::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - TB::class.java -> repository.changesOfType() + TB::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - TT::class.java -> repository.changesOfType() + TT::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - TE::class.java -> repository.changesOfType() + TE::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - PS::class.java -> repository.changesOfType() + PS::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - EPS::class.java -> repository.changesOfType() + EPS::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - GV::class.java -> repository.changesOfType() + GV::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - CAL::class.java -> repository.changesOfType() + CAL::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - UE::class.java -> repository.changesOfType() + UE::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - RM::class.java -> repository.changesOfType() + RM::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - DS::class.java -> repository.changesOfType() + DS::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - HR::class.java -> repository.changesOfType() + HR::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - SC::class.java -> repository.changesOfType() + SC::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } - FD::class.java -> repository.changesOfType() + FD::class -> repository.changesOfType() .map { list -> list.map { it.fromDb() } } else -> throw IllegalArgumentException("Unsupported observation type: ${type.simpleName}") diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt index e5fb6c06bfa2..9782efbfe1aa 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileFunctionImpl.kt @@ -74,7 +74,7 @@ class ProfileFunctionImpl @Inject constructor( init { // Populate the mirror off the main thread so the synchronous readers never block on the DB. appScope.launch { _runningICfg.value = getProfile()?.iCfg?.takeIf { it.isUsable } } - persistenceLayer.observeChanges(EPS::class.java) + persistenceLayer.observeChanges(EPS::class) .collectResilient(appScope, aapsLogger, LTag.PROFILE) { epsList -> epsList.minOfOrNull { it.timestamp }?.let { timestamp -> // Cache keys are rounded down to the second (see getProfile), so compare against diff --git a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileSwitchExpiryScheduler.kt b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileSwitchExpiryScheduler.kt index 04c7b1e06e2d..25ff9895aeb9 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileSwitchExpiryScheduler.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/profile/ProfileSwitchExpiryScheduler.kt @@ -56,7 +56,7 @@ class ProfileSwitchExpiryScheduler @Inject constructor( } appScope.launch { reschedule() - persistenceLayer.observeChanges(PS::class.java).collect { reschedule() } + persistenceLayer.observeChanges(PS::class).collect { reschedule() } } } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index 315fb8a70024..f629449568af 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -138,7 +138,7 @@ class CommandQueueImplementation @Inject constructor( // consumes the one-shot ProfileSwitchSilentGate flag the scene set just before inserting its PS. merge( rxBus.toFlow(EventProfileChangeRequested::class).map { it.silent }, - persistenceLayer.observeChanges(PS::class.java).map { profileSwitchSilentGate.consumeSilent() } + persistenceLayer.observeChanges(PS::class).map { profileSwitchSilentGate.consumeSilent() } ).collectResilient(appScope, aapsLogger, LTag.PROFILE) { silent -> onProfileChanged(silent) } } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt index 73a940b4d525..8a320184abad 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplMigrationTest.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.insulin +import kotlin.reflect.KClass import app.aaps.core.data.model.ICfg import app.aaps.core.data.ue.Action import app.aaps.core.data.ue.Sources @@ -71,7 +72,7 @@ class InsulinImplMigrationTest : TestBase() { @BeforeEach fun setup() { - whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) // getProfile() is suspend & returns a nullable type → an unstubbed mock already returns null, // which is the "no active profile" case (iCfg then falls back to insulins[0]). // Deterministic, unique string per resource id — avoids depending on real translations while diff --git a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt index 51637ae5e91f..78a5a20631ad 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplSyncTest.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.insulin +import kotlin.reflect.KClass import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.UserEntryLogger @@ -47,7 +48,7 @@ class InsulinImplSyncTest : TestBase() { @BeforeEach fun setup() { - whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) whenever(rh.gs(any())).thenAnswer { "S" + it.getArgument(0) } // gs(TextRef) is a DEFAULT interface method, so a mock returns null rather than running it. whenever(rh.gs(any())).thenAnswer { diff --git a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplTest.kt index 042e5df38951..1d1ee0ccb384 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/insulin/InsulinImplTest.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.insulin +import kotlin.reflect.KClass import app.aaps.core.data.model.BS import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.R @@ -45,7 +46,7 @@ class InsulinImplTest : TestBase() { // dia 5.0 h, Peak 30 min insulinConfiguration = "{\"insulin\":[{\"insulinLabel\":\"test\",\"insulinEndTime\":18000000,\"insulinPeakTime\":1800000,\"concentration\":1.0}]}" whenever(preferences.get(StringNonKey.InsulinConfiguration)).thenReturn(insulinConfiguration) - whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(any>())).thenReturn(emptyFlow()) // Mock rh.gs() for nickname resolution (OREF_FREE_PEAK template) and buildSuffix (U100 concentration) whenever(rh.gs(eq(R.string.free_peak_oref))).thenReturn("Free-Peak Oref") whenever(rh.gs(eq(R.string.u100))).thenReturn("U100") diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileFunctionImplTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileFunctionImplTest.kt index 70e65609398f..92792993bf91 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileFunctionImplTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileFunctionImplTest.kt @@ -38,7 +38,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { if (attempts == 1) throw RuntimeException("induced upstream failure") // 2nd collection completes normally -> no further retry } - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(upstream) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(upstream) ProfileFunctionImpl( aapsLogger, preferences, rh, activePlugin, profileRepository, @@ -59,7 +59,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { @Test fun runningICfgMirrorFollowsEffectiveProfileChanges() = runTest { val changes = MutableSharedFlow>(extraBufferCapacity = 8) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(changes) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(changes) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(null) val sut = ProfileFunctionImpl( @@ -93,7 +93,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { @Test fun runningProfileWins() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(effectiveProfileSwitch) // The running EPS is authoritative — a pending switch must not override what the pump is actually using. @@ -102,7 +102,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { @Test fun withNothingRunningFallsBackToARequestedSwitch() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(null) // The shared fixture's iCfg is ICfg("", 0, 0) — degenerate, and now correctly rejected as unusable — // so give this one a real insulin, which is what the case being tested actually describes. @@ -120,7 +120,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { // repair re-runs every start and still matches the sentinel, an already-broken install heals on relaunch. @Test fun aRunningProfileCarryingTheMigrationSentinelCountsAsNothingInForce() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) val sentinelEps = effectiveProfileSwitch.copy(iCfg = ICfg(insulinLabel = "", insulinEndTime = -1, insulinPeakTime = -1, concentration = 1.0)) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(sentinelEps) whenever(persistenceLayer.getProfileSwitchActiveAt(anyLong())).thenReturn(null) @@ -131,7 +131,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { // …and the same for a requested switch that was stamped with the sentinel: still not a value. @Test fun aRequestedSwitchCarryingTheSentinelCountsAsNothingInForce() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(null) whenever(persistenceLayer.getProfileSwitchActiveAt(anyLong())) .thenReturn(profileSwitch.copy(iCfg = ICfg(insulinLabel = "", insulinEndTime = -1, insulinPeakTime = -1, concentration = 1.0))) @@ -143,7 +143,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { @Test fun theMirrorAlsoRejectsTheSentinel() = runTest { val changes = MutableSharedFlow>(extraBufferCapacity = 8) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(changes) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(changes) val sentinelEps = effectiveProfileSwitch.copy(iCfg = ICfg(insulinLabel = "", insulinEndTime = -1, insulinPeakTime = -1, concentration = 1.0)) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(sentinelEps) @@ -157,7 +157,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { @Test fun withNothingRunningOrRequestedItIsNull() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenReturn(null) whenever(persistenceLayer.getProfileSwitchActiveAt(anyLong())).thenReturn(null) @@ -172,7 +172,7 @@ class ProfileFunctionImplTest : TestBaseWithProfile() { // dropped and the whole window shares the first one. @Test fun secondsInOneWindowShareASingleCanonicalEps() = runTest { - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) // Fresh instance per call (same id), mirroring fromDb()'s deep copy. whenever(persistenceLayer.getEffectiveProfileSwitchActiveAt(anyLong())).thenAnswer { effectiveProfileSwitch.copy() } diff --git a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt index d57ac9cf5d47..8e85b49c39de 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/profile/ProfileSwitchExpirySchedulerTest.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.profile +import kotlin.reflect.KClass import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.ICfg import app.aaps.core.data.model.PS @@ -50,7 +51,7 @@ class ProfileSwitchExpirySchedulerTest : TestBase() { fun prepare() { whenever(dateUtil.now()).thenReturn(now) whenever(config.AAPSCLIENT).thenReturn(false) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) // UNDISPATCHED so the collector is subscribed before the scheduler under test sends anything; // RxBus has no replay, so a scheduled collector would miss those events. collector = CoroutineScope(Dispatchers.Unconfined).launch(start = CoroutineStart.UNDISPATCHED) { @@ -140,7 +141,7 @@ class ProfileSwitchExpirySchedulerTest : TestBase() { @Test fun `a PS change cancels the previous timer before it fires`() = runTest(testDispatcher) { val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(PS::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(PS::class))).thenReturn(flow) whenever(persistenceLayer.getProfileSwitchActiveAt(anyLong())).thenReturn(tempPs(now, T.mins(30).msecs())) scheduler.start() diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index f478854644c2..7f3d5baa9acd 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -3,6 +3,7 @@ package app.aaps.implementation.queue import android.content.Context import android.os.PowerManager import androidx.compose.ui.text.font.FontWeight +import kotlin.reflect.KClass import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.interfaces.alerts.LocalAlertUtils @@ -110,7 +111,7 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { @BeforeEach fun prepare() { runTest { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) commandQueue = CommandQueueMocked( aapsLogger, rxBus, diff --git a/implementation/src/test/kotlin/app/aaps/implementation/scenes/ActiveSceneManagerTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/scenes/ActiveSceneManagerTest.kt index a7b0505e17ae..e50b4ad94f25 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/scenes/ActiveSceneManagerTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/scenes/ActiveSceneManagerTest.kt @@ -31,10 +31,10 @@ class ActiveSceneManagerTest : TestBase() { private fun manager(): ActiveSceneManager { whenever(preferences.get(StringNonKey.ActiveScene)).thenReturn("") - whenever(persistenceLayer.observeChanges(TT::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(PS::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(RM::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TT::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(PS::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(RM::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TE::class)).thenReturn(emptyFlow()) return ActiveSceneManager(preferences, sceneRepository, persistenceLayer, aapsLogger) } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index fc89e1bb49d7..9c13becfc114 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -174,7 +174,7 @@ class LoopPlugin @Inject constructor( super.onStart() handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) // TempTarget changes - persistenceLayer.observeChanges(TT::class.java) + persistenceLayer.observeChanges(TT::class) // Skip db change of ending previous TT .debounce(10_000L) // try/catch keeps this app-lifetime subscription alive: an uncaught throw in onEach would diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpiryScheduler.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpiryScheduler.kt index ceb637da788a..5de11e405813 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpiryScheduler.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpiryScheduler.kt @@ -48,7 +48,7 @@ class RunningModeExpiryScheduler @Inject constructor( } appScope.launch { rescheduleFromCurrentMode() - persistenceLayer.observeChanges(RM::class.java).collect { _ -> + persistenceLayer.observeChanges(RM::class).collect { _ -> rescheduleFromCurrentMode() } } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconciler.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconciler.kt index 2678cdfe67bc..2618c5655031 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconciler.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconciler.kt @@ -72,7 +72,7 @@ class RunningModeReconciler @Inject constructor( } observerJob = appScope.launch { reconcileStartup() - persistenceLayer.observeChanges(RM::class.java).collect { _ -> + persistenceLayer.observeChanges(RM::class).collect { _ -> onAnyChange() } } diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpirySchedulerTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpirySchedulerTest.kt index 5607a638e54a..da6a21add385 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpirySchedulerTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeExpirySchedulerTest.kt @@ -3,6 +3,7 @@ package app.aaps.plugins.aps.loop.runningMode import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager +import kotlin.reflect.KClass import app.aaps.core.data.model.RM import app.aaps.core.data.time.T import app.aaps.core.interfaces.configuration.Config @@ -40,7 +41,7 @@ class RunningModeExpirySchedulerTest : TestBase() { fun prepare() { whenever(dateUtil.now()).thenReturn(now) whenever(config.APS).thenReturn(true) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) scheduler = RunningModeExpiryScheduler( persistenceLayer = persistenceLayer, workManager = workManager, @@ -99,7 +100,7 @@ class RunningModeExpirySchedulerTest : TestBase() { val duration = T.mins(30).msecs() val disconnect = temporary(RM.Mode.DISCONNECTED_PUMP, timestamp = now, durationMs = duration) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(workingMode) scheduler.start() @@ -121,7 +122,7 @@ class RunningModeExpirySchedulerTest : TestBase() { val disconnect = temporary(RM.Mode.DISCONNECTED_PUMP, timestamp = now, durationMs = duration) val working = permanent(RM.Mode.CLOSED_LOOP) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(disconnect) scheduler.start() diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconcilerTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconcilerTest.kt index b0c4b264456c..a0d6114cdcac 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconcilerTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/runningMode/RunningModeReconcilerTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.aps.loop.runningMode +import kotlin.reflect.KClass import app.aaps.core.data.model.EB import app.aaps.core.data.model.RM import app.aaps.core.data.model.TB @@ -41,7 +42,7 @@ class RunningModeReconcilerTest : TestBaseWithProfile() { @BeforeEach fun prepare() { whenever(config.APS).thenReturn(true) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) runBlocking { whenever(commandQueue.cancelTempBasal(anyBoolean(), anyBoolean())).thenReturn(pumpEnactResultProvider.get().success(true)) whenever(commandQueue.tempBasalAbsolute(anyDouble(), anyInt(), anyBoolean(), anyOrNull(), anyOrNull())).thenReturn(pumpEnactResultProvider.get().success(true)) @@ -168,7 +169,7 @@ class RunningModeReconcilerTest : TestBaseWithProfile() { val workingModeRm = workingMode(RM.Mode.CLOSED_LOOP) val disconnect = temporaryMode(RM.Mode.DISCONNECTED_PUMP, timestamp = nowValue, durationMs = T.mins(30).msecs()) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(workingModeRm) whenever(processedTbrEbData.getTempBasalIncludingConvertedExtended(anyLong())).thenReturn(null) whenever(persistenceLayer.getExtendedBolusActiveAt(anyLong())).thenReturn(null) @@ -196,7 +197,7 @@ class RunningModeReconcilerTest : TestBaseWithProfile() { isAbsolute = true, rate = 0.0, duration = T.mins(60).msecs() ) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(activeDisc) whenever(processedTbrEbData.getTempBasalIncludingConvertedExtended(anyLong())).thenReturn(zeroTbr) whenever(persistenceLayer.getExtendedBolusActiveAt(anyLong())).thenReturn(null) @@ -221,7 +222,7 @@ class RunningModeReconcilerTest : TestBaseWithProfile() { isAbsolute = true, rate = 1.5, duration = T.mins(30).msecs() ) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(workingModeRm) whenever(processedTbrEbData.getTempBasalIncludingConvertedExtended(anyLong())).thenReturn(null) @@ -252,7 +253,7 @@ class RunningModeReconcilerTest : TestBaseWithProfile() { isEmulatingTempBasal = false ) val flow = MutableSharedFlow>(replay = 0) - whenever(persistenceLayer.observeChanges(eq(RM::class.java))).thenReturn(flow) + whenever(persistenceLayer.observeChanges(eq(RM::class))).thenReturn(flow) whenever(persistenceLayer.getRunningModeActiveAt(anyLong())).thenReturn(workingModeRm) whenever(processedTbrEbData.getTempBasalIncludingConvertedExtended(anyLong())).thenReturn(null) whenever(persistenceLayer.getExtendedBolusActiveAt(anyLong())).thenReturn(null) diff --git a/plugins/calibration/src/test/kotlin/app/aaps/plugins/calibration/compose/CalibrationViewModelTest.kt b/plugins/calibration/src/test/kotlin/app/aaps/plugins/calibration/compose/CalibrationViewModelTest.kt index 6ab69e72dad0..352adb4613ce 100644 --- a/plugins/calibration/src/test/kotlin/app/aaps/plugins/calibration/compose/CalibrationViewModelTest.kt +++ b/plugins/calibration/src/test/kotlin/app/aaps/plugins/calibration/compose/CalibrationViewModelTest.kt @@ -46,8 +46,8 @@ internal class CalibrationViewModelTest { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) // Stub every collaborator the init recompute touches when no session exists. - whenever(persistenceLayer.observeChanges(CAL::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(CAL::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TE::class)).thenReturn(emptyFlow()) whenever(dateUtil.now()).thenReturn(0L) whenever(profileUtil.units).thenReturn(GlucoseUnit.MGDL) } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt index 152e1b5df154..b24a4f5ebc62 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt @@ -127,7 +127,7 @@ class IobCobCalculatorPlugin @Inject constructor( scheduleHistoryDataChange(invalidateFrom, reloadBgData = true, triggeredByNewBG = false) } // EffectiveProfileSwitch changes - persistenceLayer.observeChanges(EPS::class.java) + persistenceLayer.observeChanges(EPS::class) .onEach { epsList -> epsList.minOfOrNull { it.timestamp }?.let { timestamp -> newHistoryData(timestamp, bgDataReload = false, triggeredByNewBG = false) @@ -145,26 +145,26 @@ class IobCobCalculatorPlugin @Inject constructor( preferences.observe(DoubleKey.AutosensMin).drop(1).map {}, ).onEach { resetDataAndRunCalculation("onPreferenceChange") }.launchIn(newScope) // GlucoseValue changes → reload BG data + trigger loop - persistenceLayer.observeChanges(GV::class.java) + persistenceLayer.observeChanges(GV::class) .onEach { gvList -> gvList.minOfOrNull { it.timestamp }?.let { timestamp -> scheduleHistoryDataChange(timestamp, reloadBgData = true, triggeredByNewBG = true) } }.launchIn(newScope) // Treatment changes → invalidate caches - persistenceLayer.observeChanges(CA::class.java) + persistenceLayer.observeChanges(CA::class) .onEach { list -> list.minOfOrNull { it.timestamp }?.let { scheduleHistoryDataChange(it, reloadBgData = false) } } .launchIn(newScope) - persistenceLayer.observeChanges(BS::class.java) + persistenceLayer.observeChanges(BS::class) .onEach { list -> list.minOfOrNull { it.timestamp }?.let { scheduleHistoryDataChange(it, reloadBgData = false) } } .launchIn(newScope) - persistenceLayer.observeChanges(BCR::class.java) + persistenceLayer.observeChanges(BCR::class) .onEach { list -> list.minOfOrNull { it.timestamp }?.let { scheduleHistoryDataChange(it, reloadBgData = false) } } .launchIn(newScope) - persistenceLayer.observeChanges(TB::class.java) + persistenceLayer.observeChanges(TB::class) .onEach { list -> list.minOfOrNull { it.timestamp }?.let { scheduleHistoryDataChange(it, reloadBgData = false) } } .launchIn(newScope) - persistenceLayer.observeChanges(EB::class.java) + persistenceLayer.observeChanges(EB::class) .onEach { list -> list.minOfOrNull { it.timestamp }?.let { scheduleHistoryDataChange(it, reloadBgData = false) } } .launchIn(newScope) // Units change diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt index abb47c370bda..6deca1757709 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt @@ -242,7 +242,7 @@ class UnscentedKalmanFilterPlugin @Inject constructor( scope = newScope // Subscribe to therapy events and load initial sensor state. - persistenceLayer.observeChanges(TE::class.java) + persistenceLayer.observeChanges(TE::class) .onEach { checkForSensorChange() } diff --git a/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceViewModelTest.kt b/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceViewModelTest.kt index ecb1524cbb03..15123ecd7147 100644 --- a/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceViewModelTest.kt +++ b/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceViewModelTest.kt @@ -45,7 +45,7 @@ internal class BgSourceViewModelTest { // observeBgChanges() collects this in init; an empty flow completes immediately and never // re-triggers loadData(). The initial loadData()'s background IO launch is intentionally left // unstubbed: it never mutates the structural fields asserted below. - whenever(persistenceLayer.observeChanges(GV::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(GV::class)).thenReturn(emptyFlow()) sut = BgSourceViewModel(persistenceLayer, rh, dateUtil, profileUtil, aapsLogger, rxBus) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 51fc61dd12ff..bc63c53a3825 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -137,7 +137,7 @@ class GarminPlugin @Inject constructor( preferences.observe(GarminStringKey.RequestKey) .drop(1) .collectResilient(scope, aapsLogger, LTag.GARMIN) { sendPhoneAppMessage() } - persistenceLayer.observeChanges(GV::class.java) + persistenceLayer.observeChanges(GV::class) .collectResilient(scope, aapsLogger, LTag.GARMIN, block = ::onNewBloodGlucose) setupHttpServer() if (garminAapsKey.isNotEmpty()) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index 81b76c013176..5cc8ad609608 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -106,7 +106,7 @@ class TidepoolPlugin @Inject constructor( // Pass to setup wizard rxBus.send(EventSWSyncStatus(event.status)) } - persistenceLayer.observeChanges(GV::class.java) + persistenceLayer.observeChanges(GV::class) .collectResilient(scope, aapsLogger, LTag.TIDEPOOL) { gvList -> gvList.maxByOrNull { it.timestamp }?.let { gv -> if (gv.timestamp < uploadChunk.getLastEnd()) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3PluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3PluginTest.kt index 53f2c6506947..1ac9099fdcd4 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3PluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3PluginTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.nsclientV3 +import kotlin.reflect.KClass import app.aaps.core.data.model.BCR import app.aaps.core.data.model.BS import app.aaps.core.data.model.CA @@ -86,7 +87,7 @@ internal class NSClientV3PluginTest : TestBaseWithProfile() { @BeforeEach fun prepare() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverDelegate.connectivityStatusFlow).thenReturn(MutableStateFlow(ReceiverDelegate.ConnectivityStatus("", allowed = false, connected = false))) storeDataForDb = StoreDataForDbImpl(aapsLogger, persistenceLayer, preferences, config, nsClientRepository, CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorkerTest.kt index 181dfd5b5159..38651abe3e16 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadBgWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.data.model.GV import app.aaps.core.data.model.IDs import app.aaps.core.data.model.SourceSensor @@ -80,7 +81,7 @@ internal class LoadBgWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { whenever(nsClientSource.isEnabled()).thenReturn(true) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadDeviceStatusWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadDeviceStatusWorkerTest.kt index f5a814ce63c7..085e788ee919 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadDeviceStatusWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadDeviceStatusWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.data.time.T import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L @@ -75,7 +76,7 @@ internal class LoadDeviceStatusWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadFoodsWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadFoodsWorkerTest.kt index 00cda76d676b..325f91e2fc27 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadFoodsWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadFoodsWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L import app.aaps.core.interfaces.logging.UserEntryLogger @@ -73,7 +74,7 @@ internal class LoadFoodsWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadLastModificationWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadLastModificationWorkerTest.kt index ff2b33f30209..f756d9368a7d 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadLastModificationWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadLastModificationWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L import app.aaps.core.interfaces.logging.UserEntryLogger @@ -65,7 +66,7 @@ internal class LoadLastModificationWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt index fc250651ee66..54412920fe6d 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadProfileStoreWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L import app.aaps.core.interfaces.logging.UserEntryLogger @@ -74,7 +75,7 @@ internal class LoadProfileStoreWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadStatusWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadStatusWorkerTest.kt index 8367c4d054fb..13f30eebd750 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadStatusWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadStatusWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L import app.aaps.core.interfaces.logging.UserEntryLogger @@ -70,7 +71,7 @@ internal class LoadStatusWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { nsClientMvvmRepository = NSClientRepositoryImpl(rxBus, aapsLogger) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorkerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorkerTest.kt index 6fe437132fee..411da3b19120 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorkerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/workers/LoadTreatmentsWorkerTest.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import androidx.work.WorkerFactory import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder +import kotlin.reflect.KClass import app.aaps.core.data.model.CA import app.aaps.core.data.model.IDs import app.aaps.core.interfaces.db.PersistenceLayer @@ -72,7 +73,7 @@ internal class LoadTreatmentsWorkerTest : TestBaseWithProfile() { @BeforeEach fun setUp() { - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) whenever(persistenceLayer.observeAnyChange()).thenReturn(emptyFlow()) whenever(receiverStatusStore.networkStatusFlow).thenReturn(MutableStateFlow(null)) whenever(receiverStatusStore.chargingStatusFlow).thenReturn(MutableStateFlow(null)) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt index 002eb9eb2ffb..ded402fe6cc0 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPluginTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.tidepool +import kotlin.reflect.KClass import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.L import app.aaps.core.interfaces.ui.UiInteraction @@ -45,7 +46,7 @@ class TidepoolPluginTest : TestBaseWithProfile() { @BeforeEach fun prepare() { rateLimit = RateLimit(dateUtil) whenever(receiverDelegate.connectivityStatusFlow).thenReturn(connectivityFlow) - whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(anyOrNull>())).thenReturn(emptyFlow()) tidepoolPlugin = TidepoolPlugin( aapsLogger, rh, preferences, rxBus, tidepoolUploader, uploadChunk, rateLimit, receiverDelegate, authFlowOut, tidepoolRepository, dateUtil, persistenceLayer ) diff --git a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt index e5d4200bf118..ca87f54dfc71 100644 --- a/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt +++ b/pump/dana/src/main/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModel.kt @@ -102,10 +102,10 @@ open class DanaOverviewViewModel @Inject constructor( .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } // Observe EB/TB database changes for immediate UI updates - persistenceLayer.observeChanges(EB::class.java) + persistenceLayer.observeChanges(EB::class) .onEach { rxTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) - persistenceLayer.observeChanges(TB::class.java) + persistenceLayer.observeChanges(TB::class) .onEach { rxTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) } diff --git a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt index 9e3f500cb34b..760c653043c8 100644 --- a/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt +++ b/pump/dana/src/test/kotlin/app/aaps/pump/dana/compose/DanaOverviewViewModelTest.kt @@ -90,8 +90,8 @@ internal class DanaOverviewViewModelTest { whenever(rxBus.toFlow(EventDanaRNewStatus::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) // formatting collaborators (called during buildUiState before the isConfigured branch) whenever(ch.basalRateString(any(), any(), any())).thenReturn("0.00 U/h") diff --git a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt index 69aab8225e8a..bee0f55ec135 100644 --- a/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt +++ b/pump/danars/src/test/kotlin/app/aaps/pump/danars/compose/DanaRSOverviewViewModelTest.kt @@ -93,8 +93,8 @@ internal class DanaRSOverviewViewModelTest { whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventDanaRNewStatus::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) // formatting collaborators (called during buildUiState before the isConfigured branch) whenever(ch.basalRateString(any(), any(), any())).thenReturn("0.00 U/h") diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt index 20dc18ca8d33..c2699ebf450c 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModel.kt @@ -100,10 +100,10 @@ class DiaconnOverviewViewModel @Inject constructor( rxBus.toFlow(EventInitializationChanged::class) .collectResilient(viewModelScope, aapsLogger, LTag.PUMP, start = CoroutineStart.UNDISPATCHED) { rxTrigger.value = System.currentTimeMillis() } - persistenceLayer.observeChanges(EB::class.java) + persistenceLayer.observeChanges(EB::class) .onEach { rxTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) - persistenceLayer.observeChanges(TB::class.java) + persistenceLayer.observeChanges(TB::class) .onEach { rxTrigger.value = System.currentTimeMillis() } .launchIn(viewModelScope) } diff --git a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt index 1470ef206b3b..5e018ba0fd44 100644 --- a/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt +++ b/pump/diaconn/src/test/kotlin/app/aaps/pump/diaconn/compose/DiaconnOverviewViewModelTest.kt @@ -89,8 +89,8 @@ internal class DiaconnOverviewViewModelTest { whenever(rxBus.toFlow(EventDiaconnG8NewStatus::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(aapsSchedulers.io).thenReturn(Schedulers.trampoline()) - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) // formatting collaborators (called during buildUiState before the isConfigured branch) whenever(ch.fromPump(any(), any())).thenReturn(0.0) diff --git a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt index 4a5f16aae0c0..0bac4eb82284 100644 --- a/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt +++ b/pump/virtual/src/test/kotlin/app/aaps/pump/virtual/VirtualPumpViewModelTest.kt @@ -52,9 +52,9 @@ internal class VirtualPumpViewModelTest { whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(pumpStatusFlow) whenever(rxBus.toFlow(EventQueueChanged::class)).thenReturn(queueChangedFlow) // DB change flows merged into dbChanged (evaluated eagerly in the constructor) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) // plugin StateFlows used as combine() sources whenever(virtualPumpPlugin.pumpTypeFlow).thenReturn(MutableStateFlow(null)) whenever(virtualPumpPlugin.batteryPercentFlow).thenReturn(MutableStateFlow(50)) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt index c26a2b860a92..76a7294d38e4 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt @@ -96,9 +96,9 @@ class ManageViewModel @Inject constructor( private fun setupEventListeners() { rxBus.toFlow(EventInitializationChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) - persistenceLayer.observeChanges(EB::class.java) + persistenceLayer.observeChanges(EB::class) .onEach { refreshState() }.launchIn(viewModelScope) - persistenceLayer.observeChanges(TB::class.java) + persistenceLayer.observeChanges(TB::class) .onEach { refreshState() }.launchIn(viewModelScope) rxBus.toFlow(EventCustomActionsChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt index ad99c2380570..6b73cc3f34bb 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt @@ -297,7 +297,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // Observe GlucoseValue changes scope.launch { - persistenceLayer.observeChanges(GV::class.java) + persistenceLayer.observeChanges(GV::class) .compensateForClockSkew(config, dateUtil) .collect { glucoseValues -> aapsLogger.debug(LTag.UI, "GV change detected, updating BgInfo (${glucoseValues.size} values)") @@ -345,7 +345,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // Observe treatment-related DB changes for (type in listOf( - BS::class.java, CA::class.java, EB::class.java, TE::class.java + BS::class, CA::class, EB::class, TE::class )) { scope.launch { persistenceLayer.observeChanges(type) @@ -356,7 +356,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( } // Observe HR changes for treatment graph + heart rate graph scope.launch { - persistenceLayer.observeChanges(HR::class.java) + persistenceLayer.observeChanges(HR::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -366,7 +366,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( } // Observe SC changes for treatment graph + steps graph scope.launch { - persistenceLayer.observeChanges(SC::class.java) + persistenceLayer.observeChanges(SC::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -376,7 +376,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( } // Observe running mode changes for graph + chip scope.launch { - persistenceLayer.observeChanges(RM::class.java) + persistenceLayer.observeChanges(RM::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -387,7 +387,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // Observe TT changes for target line graph + chip scope.launch { - persistenceLayer.observeChanges(TT::class.java) + persistenceLayer.observeChanges(TT::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -418,7 +418,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( } // EPS changes affect EPS graph, profile chip, TT chip, target line, and basal scope.launch { - persistenceLayer.observeChanges(EPS::class.java) + persistenceLayer.observeChanges(EPS::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -433,7 +433,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( // Observe basal-related DB changes scope.launch { - persistenceLayer.observeChanges(TB::class.java) + persistenceLayer.observeChanges(TB::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { @@ -442,7 +442,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( } } scope.launch { - persistenceLayer.observeChanges(EB::class.java) + persistenceLayer.observeChanges(EB::class) .compensateForClockSkew(config, dateUtil) .debounce(300) .collect { rebuildBasalGraph() } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt index 574511ef64fa..c31ca8a085ac 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModel.kt @@ -69,7 +69,7 @@ class StatusViewModel @Inject constructor( private fun setupEventListeners() { rxBus.toFlow(EventInitializationChanged::class) .onEach { refreshState() }.launchIn(viewModelScope) - persistenceLayer.observeChanges(TE::class.java) + persistenceLayer.observeChanges(TE::class) .onEach { refreshState() }.launchIn(viewModelScope) persistenceLayer.databaseClearedFlow .onEach { refreshState() }.launchIn(viewModelScope) diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt index 4b16aa6a7a47..05c646e34cd1 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModel.kt @@ -166,7 +166,7 @@ class ProfileManagementViewModel @Inject constructor( */ @OptIn(ExperimentalCoroutinesApi::class) private fun observeActiveProfileForAutoNavigation() { - persistenceLayer.observeChanges(EPS::class.java) + persistenceLayer.observeChanges(EPS::class) .compensateForClockSkew(config, dateUtil) .onStart { emit(emptyList()) } .mapLatest { @@ -188,7 +188,7 @@ class ProfileManagementViewModel @Inject constructor( val uiState: StateFlow = combine( profileRepository.profiles, _selectedIndex, - persistenceLayer.observeChanges(EPS::class.java).compensateForClockSkew(config, dateUtil).onStart { emit(emptyList()) }, + persistenceLayer.observeChanges(EPS::class).compensateForClockSkew(config, dateUtil).onStart { emit(emptyList()) }, _screenMode, // An input, not a snapshot: pairing/unpairing or the master going away while this screen is // open has to move it between editable and read-only right away. diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModel.kt index fcc555aef7c4..2db57b67e557 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModel.kt @@ -75,7 +75,7 @@ class SiteRotationManagementViewModel @Inject constructor( } private fun setupEventListeners() { - persistenceLayer.observeChanges(TE::class.java) + persistenceLayer.observeChanges(TE::class) .onEach { loadEntries() } .launchIn(viewModelScope) // The settings bottom sheet writes the body-type pref directly (generic preference renderer), so observe it diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/foodManagement/FoodManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/foodManagement/FoodManagementViewModelTest.kt index 797b0f917554..3a68698b6bf4 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/foodManagement/FoodManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/foodManagement/FoodManagementViewModelTest.kt @@ -32,7 +32,7 @@ internal class FoodManagementViewModelTest { // stays clean and we test the synchronous update methods against the default state. The observeChanges() // cold flow is still built synchronously during init, so it must be stubbed non-null. Dispatchers.setMain(StandardTestDispatcher()) - whenever(persistenceLayer.observeChanges(FD::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(FD::class)).thenReturn(emptyFlow()) sut = FoodManagementViewModel(persistenceLayer, aapsLogger) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModelTest.kt index 0f69fbf18190..2cd64b3573da 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementViewModelTest.kt @@ -71,7 +71,7 @@ internal class InsulinManagementViewModelTest { whenever(preferences.get(StringNonKey.InsulinConfiguration)).thenReturn("") // Cold flow chains built synchronously in observeProfileChanges()/observeConfigChanges(). whenever(profileRepository.profiles).thenReturn(MutableStateFlow(emptyList())) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) whenever(preferences.observe(StringNonKey.InsulinConfiguration)).thenReturn(configFlow) sut = InsulinManagementViewModel( insulinManager, preferences, profileFunction, dateUtil, hardLimits, uel, diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt index 6196c4c512e9..fd63ff390e99 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModelTest.kt @@ -74,8 +74,8 @@ internal class ManageViewModelTest { // so the uiState stays at ManageUiState defaults and we test the synchronous methods. Dispatchers.setMain(StandardTestDispatcher()) // setupEventListeners() builds cold flow chains synchronously in init — every source must be non-null. - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventCustomActionsChanged::class)).thenReturn(emptyFlow()) whenever(nsClient.masterOrPairedClientFlow).thenReturn(MutableStateFlow(false)) diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt index 1572302ca56e..234d793f9b91 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/overview/statusLights/StatusViewModelTest.kt @@ -56,7 +56,7 @@ internal class StatusViewModelTest { whenever(rxBus.toFlow(EventInitializationChanged::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventPumpStatusChanged::class)).thenReturn(emptyFlow()) whenever(rxBus.toFlow(EventNsClientStatusUpdated::class)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TE::class)).thenReturn(emptyFlow()) whenever(persistenceLayer.databaseClearedFlow).thenReturn(emptyFlow()) sut = StatusViewModel( rh, activePlugin, profileFunction, config, persistenceLayer, dateUtil, rxBus, preferences, diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileHelperViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileHelperViewModelTest.kt index 19a1e5908dd7..6fa2ba49d339 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileHelperViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileHelperViewModelTest.kt @@ -52,7 +52,7 @@ internal class ProfileHelperViewModelTest { // coroutines (no advanceUntilIdle), so construction stays clean and we test the synchronous getters // against the default uiState. Only the EPS observeChanges cold flow chain is built synchronously. Dispatchers.setMain(StandardTestDispatcher()) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) sut = ProfileHelperViewModel( persistenceLayer, profileRepository, profileFunction, profileUtil, rh, dateUtil, tddCalculator, defaultProfile, defaultProfileDPV, rxBus, fabricPrivacy diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt index e0aa426e1708..2e3dfee19336 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/profileManagement/viewmodels/ProfileManagementViewModelTest.kt @@ -93,7 +93,7 @@ internal class ProfileManagementViewModelTest { // and in the uiState combine. Dispatchers.setMain(StandardTestDispatcher()) whenever(profileRepository.profiles).thenReturn(profilesFlow) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) // Pairing and reachability are inputs of uiState, so the combine needs real flows here. whenever(nsClient.masterOrPairedClientFlow).thenReturn(MutableStateFlow(true)) whenever(nsClient.masterReachable).thenReturn(MutableStateFlow(true)) diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModelTest.kt index 2df5d50aaa6c..93f157ddcb19 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/runningMode/RunningModeManagementViewModelTest.kt @@ -54,8 +54,8 @@ internal class RunningModeManagementViewModelTest { // loadState() launch + the observer launchIns are deferred by StandardTestDispatcher; the observed flows // are still built, so they must be non-null. Dispatchers.setMain(StandardTestDispatcher()) - whenever(persistenceLayer.observeChanges(RM::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(RM::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) sut = RunningModeManagementViewModel( loop, activePlugin, profileFunction, translator, preferences, persistenceLayer, aapsLogger, rxBus, rh, dateUtil, config, batchExecutor, CoroutineScope(UnconfinedTestDispatcher()) diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModelTest.kt index 561cb62ec120..d4c2384ffdcf 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/siteRotationDialog/viewModels/SiteRotationManagementViewModelTest.kt @@ -47,7 +47,7 @@ internal class SiteRotationManagementViewModelTest { // stays clean and we test the synchronous setters against the default state. The cold flow chains built // synchronously in setupEventListeners() must be stubbed to non-null flows or construction NPEs. Dispatchers.setMain(StandardTestDispatcher()) - whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TE::class)).thenReturn(emptyFlow()) whenever(preferences.observe(IntKey.SiteRotationUserProfile)).thenReturn(MutableStateFlow(0)) whenever(preferences.get(IntKey.SiteRotationUserProfile)).thenReturn(0) sut = SiteRotationManagementViewModel( diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModelTest.kt index ecfdb93379a3..71de361753af 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/tempTarget/TempTargetManagementViewModelTest.kt @@ -64,7 +64,7 @@ internal class TempTargetManagementViewModelTest { MockitoAnnotations.openMocks(this) Dispatchers.setMain(StandardTestDispatcher()) // init observers must be built (their launchIn is deferred); loadData() launch is deferred too. - whenever(persistenceLayer.observeChanges(TT::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TT::class)).thenReturn(emptyFlow()) whenever(preferences.observe(StringNonKey.TempTargetPresets)).thenReturn(MutableStateFlow("[]")) sut = TempTargetManagementViewModel( persistenceLayer, profileFunction, profileUtil, preferences, rh, dateUtil, aapsLogger, diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/BolusCarbsViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/BolusCarbsViewModelTest.kt index bfacd0bcd49e..ff6ea05c9540 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/BolusCarbsViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/BolusCarbsViewModelTest.kt @@ -50,9 +50,9 @@ internal class BolusCarbsViewModelTest { // observeTreatmentChanges() merges these three in init; empty flows complete and never re-trigger. // The initial loadData() background load is intentionally left unstubbed — it fails and is swallowed // by the VM's try/catch, never mutating the structural fields asserted below. - whenever(persistenceLayer.observeChanges(BS::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(CA::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(BCR::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(BS::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(CA::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(BCR::class)).thenReturn(emptyFlow()) sut = BolusCarbsViewModel(persistenceLayer, profileFunction, rh, dateUtil, decimalFormatter, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/CareportalViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/CareportalViewModelTest.kt index 576ebeab7a17..c522b0194d10 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/CareportalViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/CareportalViewModelTest.kt @@ -39,7 +39,7 @@ internal class CareportalViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(TE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TE::class)).thenReturn(emptyFlow()) sut = CareportalViewModel(persistenceLayer, rh, translator, dateUtil, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ExtendedBolusViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ExtendedBolusViewModelTest.kt index d9fb8ae6d500..03991a315a5f 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ExtendedBolusViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ExtendedBolusViewModelTest.kt @@ -37,7 +37,7 @@ internal class ExtendedBolusViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(EB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EB::class)).thenReturn(emptyFlow()) sut = ExtendedBolusViewModel(persistenceLayer, rh, dateUtil, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ProfileSwitchViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ProfileSwitchViewModelTest.kt index 7733e24b738d..51d3f57aa7bd 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ProfileSwitchViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/ProfileSwitchViewModelTest.kt @@ -44,8 +44,8 @@ internal class ProfileSwitchViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(PS::class.java)).thenReturn(emptyFlow()) - whenever(persistenceLayer.observeChanges(EPS::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(PS::class)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(EPS::class)).thenReturn(emptyFlow()) sut = ProfileSwitchViewModel(persistenceLayer, profileRepository, rh, dateUtil, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/RunningModeViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/RunningModeViewModelTest.kt index d69fa26daf58..c3465a7dad76 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/RunningModeViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/RunningModeViewModelTest.kt @@ -37,7 +37,7 @@ internal class RunningModeViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(RM::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(RM::class)).thenReturn(emptyFlow()) sut = RunningModeViewModel(persistenceLayer, rh, dateUtil, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModelTest.kt index 01d17c884e13..1d6f4e0e8864 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModelTest.kt @@ -44,7 +44,7 @@ internal class TempBasalViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(TB::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TB::class)).thenReturn(emptyFlow()) sut = TempBasalViewModel(persistenceLayer, profileFunction, activePlugin, rh, dateUtil, decimalFormatter, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModelTest.kt index f781eb57abc1..5572aa0ad080 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModelTest.kt @@ -39,7 +39,7 @@ internal class TempTargetViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(TT::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(TT::class)).thenReturn(emptyFlow()) sut = TempTargetViewModel(persistenceLayer, profileUtil, rh, dateUtil, aapsLogger, rxBus) } diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/UserEntryViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/UserEntryViewModelTest.kt index 318708c7419d..7ecf7455ba04 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/UserEntryViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/treatments/viewmodels/UserEntryViewModelTest.kt @@ -35,7 +35,7 @@ internal class UserEntryViewModelTest { fun setUp() { MockitoAnnotations.openMocks(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - whenever(persistenceLayer.observeChanges(UE::class.java)).thenReturn(emptyFlow()) + whenever(persistenceLayer.observeChanges(UE::class)).thenReturn(emptyFlow()) sut = UserEntryViewModel(persistenceLayer, rh, dateUtil, aapsLogger, rxBus) } From cd18389e98cad92008f26ed1136caab8f15c4fe5 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 13:59:31 +0200 Subject: [PATCH 110/146] Delete the SP.Editor resource id overloads, used only by their own test remove, putBoolean, putDouble, putLong, putInt and putString taking a @StringRes id existed on SP.Editor and nowhere else did anything call them, except SPImplTest. The test drives them through a `someResource` variable, so a search for edit blocks containing R.string found nothing and reported them as dead. They are dead in production; the test assertions go with them. SP itself stays in androidMain with its own resource id overloads, which wear uses. That is where it belongs: Preferences in :core:keys is the platform neutral API and is already in commonMain without referencing SP, and SP is the Android SharedPreferences layer that the phone's PreferencesImpl is built on. SP is not a blocker either - nothing in :core:interfaces references it. --- .../core/interfaces/sharedPreferences/SP.kt | 6 ------ .../shared/impl/sharedPreferences/SPImpl.kt | 18 ------------------ .../impl/sharedPreferences/SPImplTest.kt | 12 ------------ 3 files changed, 36 deletions(-) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt index f7a153ae1e90..07272d52dfd6 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/sharedPreferences/SP.kt @@ -17,19 +17,13 @@ interface SP { fun clear() - fun remove(@StringRes resourceID: Int) fun remove(key: String) fun putBoolean(key: String, value: Boolean) - fun putBoolean(@StringRes resourceID: Int, value: Boolean) fun putDouble(key: String, value: Double) - fun putDouble(@StringRes resourceID: Int, value: Double) fun putLong(key: String, value: Long) - fun putLong(@StringRes resourceID: Int, value: Long) fun putInt(key: String, value: Int) - fun putInt(@StringRes resourceID: Int, value: Int) fun putString(key: String, value: String) - fun putString(@StringRes resourceID: Int, value: String) } /** diff --git a/shared/impl/src/main/kotlin/app/aaps/shared/impl/sharedPreferences/SPImpl.kt b/shared/impl/src/main/kotlin/app/aaps/shared/impl/sharedPreferences/SPImpl.kt index b55d5d515499..353fa33e1b49 100644 --- a/shared/impl/src/main/kotlin/app/aaps/shared/impl/sharedPreferences/SPImpl.kt +++ b/shared/impl/src/main/kotlin/app/aaps/shared/impl/sharedPreferences/SPImpl.kt @@ -24,9 +24,6 @@ class SPImpl @Inject constructor( spEdit.clear() } - override fun remove(@StringRes resourceID: Int) { - spEdit.remove(context.getString(resourceID)) - } override fun remove(key: String) { spEdit.remove(key) @@ -36,41 +33,26 @@ class SPImpl @Inject constructor( spEdit.putBoolean(key, value) } - override fun putBoolean(@StringRes resourceID: Int, value: Boolean) { - spEdit.putBoolean(context.getString(resourceID), value) - } override fun putDouble(key: String, value: Double) { spEdit.putString(key, value.toString()) } - override fun putDouble(@StringRes resourceID: Int, value: Double) { - spEdit.putString(context.getString(resourceID), value.toString()) - } override fun putLong(key: String, value: Long) { spEdit.putLong(key, value) } - override fun putLong(@StringRes resourceID: Int, value: Long) { - spEdit.putLong(context.getString(resourceID), value) - } override fun putInt(key: String, value: Int) { spEdit.putInt(key, value) } - override fun putInt(@StringRes resourceID: Int, value: Int) { - spEdit.putInt(context.getString(resourceID), value) - } override fun putString(key: String, value: String) { spEdit.putString(key, value) } - override fun putString(@StringRes resourceID: Int, value: String) { - spEdit.putString(context.getString(resourceID), value) - } } block(edit) diff --git a/shared/impl/src/test/kotlin/app/aaps/shared/impl/sharedPreferences/SPImplTest.kt b/shared/impl/src/test/kotlin/app/aaps/shared/impl/sharedPreferences/SPImplTest.kt index 0978ef5f527a..e068eda7f26c 100644 --- a/shared/impl/src/test/kotlin/app/aaps/shared/impl/sharedPreferences/SPImplTest.kt +++ b/shared/impl/src/test/kotlin/app/aaps/shared/impl/sharedPreferences/SPImplTest.kt @@ -38,34 +38,22 @@ class SPImplTest { assertThat(sut.getBoolean("test", false)).isTrue() sut.edit { remove("test") } assertThat(sut.contains("test")).isFalse() - sut.edit { putBoolean(someResource, true) } - assertThat(sut.getBoolean(someResource, false)).isTrue() - sut.edit { remove(someResource) } - assertThat(sut.contains(someResource)).isFalse() sut.edit(commit = true) { putDouble("test", 1.0) } assertThat(sut.getDouble("test", 2.0)).isEqualTo(1.0) - sut.edit { putDouble(someResource, 1.0) } - assertThat(sut.getDouble(someResource, 2.0)).isEqualTo(1.0) sut.edit { clear() } assertThat(sut.contains(someResource2)).isFalse() sut.edit { putInt("test", 1) } assertThat(sut.getInt("test", 2)).isEqualTo(1) - sut.edit { putInt(someResource, 1) } - assertThat(sut.getInt(someResource, 2)).isEqualTo(1) sut.edit { clear() } sut.edit { putLong("test", 1L) } assertThat(sut.getLong("test", 2L)).isEqualTo(1L) - sut.edit { putLong(someResource, 1) } - assertThat(sut.getLong(someResource, 2L)).isEqualTo(1L) sut.edit { clear() } sut.edit { putString("test", "string") } assertThat(sut.getString("test", "a")).isEqualTo("string") - sut.edit { putString(someResource, "string") } - assertThat(sut.getString(someResource, "a")).isEqualTo("string") sut.edit { clear() } } From 60a80c4ffafc91d7b520d1d4302fb8ec2652d1b4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 14:50:32 +0200 Subject: [PATCH 111/146] Profile, PumpSync and InsulinType resolve through TextResolver Same change as DateUtil, for the files that named ResourceHelper only to pass it along. Callers are untouched, because a ResourceHelper is a TextResolver. InsulinType was already halfway there: its labels are TextRefs, so rh.gs(label) was a TextResolver call already and only the parameter type had to change. It moves to commonMain and takes InsulinManager with it. Profile names it in five signatures and never calls it. The work is in ProfileSealed, which does: 17 rh.gs(R.string.x) become rh.gs(TextRef.AndroidRes(R.string.x)), including two multi line calls and one that picks the id with an if. PumpSync has three of the same in its own default methods. Profile and PumpSync still cannot move: Profile names Pump, and PumpSync names Profile. Pump itself has no platform import and no ResourceHelper, so what holds that group is worth a separate look. ProfileSealedTest stubbed rh.gs with a raw id and now stubs the TextRef, which is what turned "mmol/L/U" into null. 207 files in commonMain, 53 left. --- .../aaps/core/interfaces/profile/Profile.kt | 12 ++--- .../app/aaps/core/interfaces/pump/PumpSync.kt | 13 ++--- .../core/interfaces/insulin/InsulinManager.kt | 0 .../core/interfaces/insulin/InsulinType.kt | 4 +- .../core/objects/profile/ProfileSealed.kt | 48 +++++++++---------- .../core/objects/profile/ProfileSealedTest.kt | 9 ++-- 6 files changed, 44 insertions(+), 42 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt (90%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt index c6ee981c0bc3..130d3f627381 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt @@ -6,7 +6,7 @@ import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.pump.Pump -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.interfaces.utils.Round @@ -24,7 +24,7 @@ interface Profile { /** * Check validity of profile */ - fun isValid(from: String, pump: Pump, config: Config, rh: ResourceHelper, notificationManager: NotificationManager, hardLimits: HardLimits, sendNotifications: Boolean): ValidityCheck + fun isValid(from: String, pump: Pump, config: Config, rh: TextResolver, notificationManager: NotificationManager, hardLimits: HardLimits, sendNotifications: Boolean): ValidityCheck /** * Units used for ISF & target @@ -127,10 +127,10 @@ interface Profile { */ fun getTargetHighMgdlTimeFromMidnight(timeAsSeconds: Int): Double - fun getIcList(rh: ResourceHelper, dateUtil: DateUtil): String - fun getIsfList(rh: ResourceHelper, dateUtil: DateUtil): String - fun getBasalList(rh: ResourceHelper, dateUtil: DateUtil): String - fun getTargetList(rh: ResourceHelper, dateUtil: DateUtil): String + fun getIcList(rh: TextResolver, dateUtil: DateUtil): String + fun getIsfList(rh: TextResolver, dateUtil: DateUtil): String + fun getBasalList(rh: TextResolver, dateUtil: DateUtil): String + fun getTargetList(rh: TextResolver, dateUtil: DateUtil): String fun convertToNonCustomizedProfile(dateUtil: DateUtil): PureProfile fun toPureNsJson(dateUtil: DateUtil): JsonObject diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt index a91fd2bce69c..b4052771990a 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt @@ -1,5 +1,6 @@ package app.aaps.core.interfaces.pump +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.BS import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TB @@ -9,7 +10,7 @@ import app.aaps.core.data.time.T import app.aaps.core.data.ue.Sources import app.aaps.core.interfaces.R import app.aaps.core.interfaces.profile.Profile -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DateUtil import kotlin.math.max import kotlin.math.min @@ -99,14 +100,14 @@ interface PumpSync { if (isAbsolute) rate else profile.getBasal(time) * rate / 100 - fun toStringFull(dateUtil: DateUtil, rh: ResourceHelper): String { + fun toStringFull(dateUtil: DateUtil, rh: TextResolver): String { return when { isAbsolute -> { - rh.gs(R.string.temp_basal_absolute_rate, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) + rh.gs(TextRef.AndroidRes(R.string.temp_basal_absolute_rate), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) } else -> { // percent - rh.gs(R.string.temp_basal_percent_rate, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) + rh.gs(TextRef.AndroidRes(R.string.temp_basal_percent_rate), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) } } } @@ -138,8 +139,8 @@ interface PumpSync { private fun getPassedDurationToTimeInMinutes(time: Long): Int = ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() - fun toStringFull(dateUtil: DateUtil, rh: ResourceHelper): String = - rh.gs(R.string.temp_basal_extended_bolus, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) + fun toStringFull(dateUtil: DateUtil, rh: TextResolver): String = + rh.gs(TextRef.AndroidRes(R.string.temp_basal_extended_bolus), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/InsulinManager.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt similarity index 90% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt index 50ddb5a3ea67..1ad9b4cc9908 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/insulin/InsulinType.kt @@ -2,7 +2,7 @@ package app.aaps.core.interfaces.insulin import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.InterfacesStrings -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.keys.interfaces.TextRef enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeakTime: Long, val label: TextRef, val comment: TextRef) { @@ -19,7 +19,7 @@ enum class InsulinType(val value: Int, val insulinEndTime: Long, val insulinPeak get() = ICfg(this.name, insulinEndTime, insulinPeakTime, 1.0) /** Provide iCfg with a default friendly name on insulin creation from template */ - fun getICfg(rh: ResourceHelper): ICfg = ICfg(rh.gs(this.label), insulinEndTime, insulinPeakTime, 1.0) + fun getICfg(rh: TextResolver): ICfg = ICfg(rh.gs(this.label), insulinEndTime, insulinPeakTime, 1.0) companion object { diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index 98e25fc07741..6ec06f284dea 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -22,7 +22,7 @@ import app.aaps.core.interfaces.profile.Profile.ProfileValue import app.aaps.core.interfaces.profile.PureProfile import app.aaps.core.interfaces.pump.Pump import app.aaps.core.interfaces.pump.PumpProfile -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.HardLimits import app.aaps.core.objects.extensions.blockValueBySeconds @@ -163,7 +163,7 @@ sealed class ProfileSealed( override val iCfg = null } - override fun isValid(from: String, pump: Pump, config: Config, rh: ResourceHelper, notificationManager: NotificationManager, hardLimits: HardLimits, sendNotifications: Boolean): Profile.ValidityCheck { + override fun isValid(from: String, pump: Pump, config: Config, rh: TextResolver, notificationManager: NotificationManager, hardLimits: HardLimits, sendNotifications: Boolean): Profile.ValidityCheck { // Full validity = semantic (pump-independent) AND pump compatibility. Activating a profile // on the pump requires both; editing, local storage and Nightscout sync only require // [validateSemantic] (a profile that's merely incompatible with the *current* pump is still @@ -181,13 +181,13 @@ sealed class ProfileSealed( * the gate for editing, local storage and Nightscout sync — independent of the active pump, so * switching pumps never silently blocks profile sync. */ - fun validateSemantic(rh: ResourceHelper, hardLimits: HardLimits): Profile.ValidityCheck { + fun validateSemantic(rh: TextResolver, hardLimits: HardLimits): Profile.ValidityCheck { val validityCheck = Profile.ValidityCheck() for (basal in basalBlocks) { val basalAmount = basal.amount * percentage / 100.0 if (basalAmount !in 0.01..hardLimits.maxBasal()) { validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.value_out_of_hard_limits, rh.gs(R.string.basal_value), basalAmount)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.value_out_of_hard_limits), rh.gs(TextRef.AndroidRes(R.string.basal_value)), basalAmount)) break } } @@ -195,7 +195,7 @@ sealed class ProfileSealed( // Todo, add check for peak and concentration, (or delegate iCfg validity check to insulinPlugin which will have this function) if (it.dia !in hardLimits.diaRange()) { validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.value_out_of_hard_limits, rh.gs(R.string.profile_dia), it.dia)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.value_out_of_hard_limits), rh.gs(TextRef.AndroidRes(R.string.profile_dia)), it.dia)) } } for (ic in icBlocks) @@ -203,8 +203,8 @@ sealed class ProfileSealed( validityCheck.isValid = false validityCheck.reasons.add( rh.gs( - R.string.value_out_of_hard_limits, - rh.gs(R.string.profile_carbs_ratio_value), + TextRef.AndroidRes(R.string.value_out_of_hard_limits), + rh.gs(TextRef.AndroidRes(R.string.profile_carbs_ratio_value)), ic.amount * 100.0 / percentage ) ) @@ -215,8 +215,8 @@ sealed class ProfileSealed( validityCheck.isValid = false validityCheck.reasons.add( rh.gs( - R.string.value_out_of_hard_limits, - rh.gs(R.string.profile_sensitivity_value), + TextRef.AndroidRes(R.string.value_out_of_hard_limits), + rh.gs(TextRef.AndroidRes(R.string.profile_sensitivity_value)), isf.amount * 100.0 / percentage ) ) @@ -225,12 +225,12 @@ sealed class ProfileSealed( for (target in targetBlocks) { if (toMgdl(target.lowTarget, units) !in HardLimits.LIMIT_MIN_BG) { validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.value_out_of_hard_limits, rh.gs(R.string.profile_low_target), target.lowTarget)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.value_out_of_hard_limits), rh.gs(TextRef.AndroidRes(R.string.profile_low_target)), target.lowTarget)) break } if (toMgdl(target.highTarget, units) !in HardLimits.LIMIT_MAX_BG) { validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.value_out_of_hard_limits, rh.gs(R.string.profile_high_target), target.highTarget)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.value_out_of_hard_limits), rh.gs(TextRef.AndroidRes(R.string.profile_high_target)), target.highTarget)) break } } @@ -251,7 +251,7 @@ sealed class ProfileSealed( * consistently corrected profile anyway. Removing it is what let [app.aaps.core.data.model.data.Block] * become immutable. */ - fun validatePump(from: String, pump: Pump, config: Config, rh: ResourceHelper, notificationManager: NotificationManager, sendNotifications: Boolean): Profile.ValidityCheck { + fun validatePump(from: String, pump: Pump, config: Config, rh: TextResolver, notificationManager: NotificationManager, sendNotifications: Boolean): Profile.ValidityCheck { val validityCheck = Profile.ValidityCheck() val description = pump.pumpDescription for (basal in basalBlocks) { @@ -265,7 +265,7 @@ sealed class ProfileSealed( } validityCheck.isValid = false validityCheck.reasons.add( - rh.gs(R.string.basalprofilenotaligned, from) + rh.gs(TextRef.AndroidRes(R.string.basalprofilenotaligned), from) ) break } @@ -274,23 +274,23 @@ sealed class ProfileSealed( if (basalAmount < description.basalMinimumRate) { if (sendNotifications) sendBelowMinimumNotification(from, notificationManager, rh) validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.minimalbasalvaluereplaced, from)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.minimalbasalvaluereplaced), from)) break } else if (basalAmount > description.basalMaximumRate) { if (sendNotifications) sendAboveMaximumNotification(from, notificationManager, rh) validityCheck.isValid = false - validityCheck.reasons.add(rh.gs(R.string.maximumbasalvaluereplaced, from)) + validityCheck.reasons.add(rh.gs(TextRef.AndroidRes(R.string.maximumbasalvaluereplaced), from)) break } } return validityCheck } - protected open fun sendBelowMinimumNotification(from: String, notificationManager: NotificationManager, rh: ResourceHelper) { + protected open fun sendBelowMinimumNotification(from: String, notificationManager: NotificationManager, rh: TextResolver) { notificationManager.post(NotificationId.MINIMAL_BASAL_VALUE_REPLACED, TextRef.AndroidRes(R.string.minimalbasalvaluereplaced, listOf(from))) } - protected open fun sendAboveMaximumNotification(from: String, notificationManager: NotificationManager, rh: ResourceHelper) { + protected open fun sendAboveMaximumNotification(from: String, notificationManager: NotificationManager, rh: TextResolver) { notificationManager.post(NotificationId.MAXIMUM_BASAL_VALUE_REPLACED, TextRef.AndroidRes(R.string.maximumbasalvaluereplaced, listOf(from))) } @@ -367,16 +367,16 @@ sealed class ProfileSealed( private fun getTargetHighTimeFromMidnight(timeAsSeconds: Int): Double = targetBlocks.highTargetBlockValueBySeconds(timeAsSeconds, timeshift) override fun getTargetHighMgdlTimeFromMidnight(timeAsSeconds: Int): Double = toMgdl(targetBlocks.highTargetBlockValueBySeconds(timeAsSeconds, timeshift), units) - override fun getIcList(rh: ResourceHelper, dateUtil: DateUtil): String = - getValuesList(icBlocks, 100.0 / percentage, NumberFormat.DECIMAL_1, rh.gs(R.string.profile_carbs_per_unit), dateUtil) + override fun getIcList(rh: TextResolver, dateUtil: DateUtil): String = + getValuesList(icBlocks, 100.0 / percentage, NumberFormat.DECIMAL_1, rh.gs(TextRef.AndroidRes(R.string.profile_carbs_per_unit)), dateUtil) - override fun getIsfList(rh: ResourceHelper, dateUtil: DateUtil): String = - getValuesList(isfBlocks, 100.0 / percentage, NumberFormat.DECIMAL_1, rh.gs(if (units == GlucoseUnit.MGDL) R.string.profile_isf_units_mgdl else R.string.profile_isf_units_mmol), dateUtil) + override fun getIsfList(rh: TextResolver, dateUtil: DateUtil): String = + getValuesList(isfBlocks, 100.0 / percentage, NumberFormat.DECIMAL_1, rh.gs(TextRef.AndroidRes(if (units == GlucoseUnit.MGDL) R.string.profile_isf_units_mgdl else R.string.profile_isf_units_mmol)), dateUtil) - override fun getBasalList(rh: ResourceHelper, dateUtil: DateUtil): String = - getValuesList(basalBlocks, percentage / 100.0, NumberFormat.DECIMAL_2, rh.gs(R.string.profile_ins_units_per_hour), dateUtil) + override fun getBasalList(rh: TextResolver, dateUtil: DateUtil): String = + getValuesList(basalBlocks, percentage / 100.0, NumberFormat.DECIMAL_2, rh.gs(TextRef.AndroidRes(R.string.profile_ins_units_per_hour)), dateUtil) - override fun getTargetList(rh: ResourceHelper, dateUtil: DateUtil): String = getTargetValuesList(targetBlocks, NumberFormat.DECIMAL_1, units.displayLabel, dateUtil) + override fun getTargetList(rh: TextResolver, dateUtil: DateUtil): String = getTargetValuesList(targetBlocks, NumberFormat.DECIMAL_1, units.displayLabel, dateUtil) override fun convertToNonCustomizedProfile(dateUtil: DateUtil): PureProfile = PureProfile( diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt index 8cb934d2d98c..7d2f169b5163 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt @@ -1,6 +1,7 @@ package app.aaps.core.objects.profile import android.content.Context +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.interfaces.aps.APS import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.notifications.NotificationManager @@ -64,10 +65,10 @@ class ProfileSealedTest : TestBase() { dateUtil = DateUtilImpl(context) hardLimits = HardLimitsMock(preferences, rh) whenever(activePlugin.activePump).thenReturn(testPumpPlugin) - whenever(rh.gs(app.aaps.core.ui.R.string.profile_isf_units_mgdl)).thenReturn("mg/dL/U") - whenever(rh.gs(app.aaps.core.ui.R.string.profile_isf_units_mmol)).thenReturn("mmol/L/U") - whenever(rh.gs(app.aaps.core.ui.R.string.profile_carbs_per_unit)).thenReturn("g/U") - whenever(rh.gs(app.aaps.core.ui.R.string.profile_ins_units_per_hour)).thenReturn("U/h") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_isf_units_mgdl))).thenReturn("mg/dL/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_isf_units_mmol))).thenReturn("mmol/L/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_carbs_per_unit))).thenReturn("g/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_ins_units_per_hour))).thenReturn("U/h") whenever(rh.gs(anyInt(), anyString())).thenReturn("") whenever(activePlugin.activeAPS).thenReturn(aps) } From 2bcf8e381c4f662ce8e111907ab91a777e85115a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 18:40:00 +0200 Subject: [PATCH 112/146] Pump, Profile, PumpSync and PumpProfile move to commonMain ImageVector was never blocking PluginDescription. Compose Multiplatform is already a commonMain dependency of this module, so it resolves there, and moving the file proves it. I had tested that earlier and then repeated the wrong reason twice. What actually held this group: - Pump waited on PumpProfile, which waited on Profile, which is the TextResolver change from the previous commit. Nothing platform bound anywhere in the chain. - PumpSync named R.string directly, so wrapping the ids in TextRef.AndroidRes did not help - AndroidRes still needs the R class. Its three strings exist in the generated common table, so they become InterfacesStrings.temp_basal_* , which is TextRef.Named and needs no R. This is the pattern InsulinType already used. PluginDescription still cannot move, but not for the reason I gave: it takes a (PluginBase) -> Any provider, and PluginBase stays behind for two small JVM things - an org.jetbrains @TestOnly annotation, and pluginId reading javaClass.simpleName. pluginId is deliberately left alone. Its KDoc says it matches the legacy RunningConfiguration encoding so the active-plugin sync keeps dual-writing the same value, and this::class.simpleName is nullable where javaClass.simpleName is not. Changing a persisted identity needs a decision, not a sweep. 211 files in commonMain, 49 left. --- .../kotlin/app/aaps/core/interfaces/profile/Profile.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/Pump.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/PumpSync.kt | 8 ++++---- 4 files changed, 4 insertions(+), 4 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/Profile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/Pump.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt (97%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/Profile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/Pump.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpProfile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt similarity index 97% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt index b4052771990a..9c7012bec27c 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt @@ -8,7 +8,7 @@ import app.aaps.core.data.model.TE import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.time.T import app.aaps.core.data.ue.Sources -import app.aaps.core.interfaces.R +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DateUtil @@ -103,11 +103,11 @@ interface PumpSync { fun toStringFull(dateUtil: DateUtil, rh: TextResolver): String { return when { isAbsolute -> { - rh.gs(TextRef.AndroidRes(R.string.temp_basal_absolute_rate), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) + rh.gs(InterfacesStrings.temp_basal_absolute_rate, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) } else -> { // percent - rh.gs(TextRef.AndroidRes(R.string.temp_basal_percent_rate), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) + rh.gs(InterfacesStrings.temp_basal_percent_rate, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), durationInMinutes) } } } @@ -140,7 +140,7 @@ interface PumpSync { ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() fun toStringFull(dateUtil: DateUtil, rh: TextResolver): String = - rh.gs(TextRef.AndroidRes(R.string.temp_basal_extended_bolus), rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) + rh.gs(InterfacesStrings.temp_basal_extended_bolus, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) } From 8e8feb3e7697f378939af851fde53fb3300925f8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 18:58:06 +0200 Subject: [PATCH 113/146] PluginBase.pluginId uses KClass, and drops @TestOnly javaClass.simpleName becomes this::class.simpleName, which is the same string: a plugin is always a named class, as you said. It is not defaulted - simpleName is only null for an anonymous class, and syncing an empty plugin id would be worse than failing, since the value is persisted and mirrored to the client. The org.jetbrains @TestOnly annotation goes too. It is JVM only and carries no runtime behaviour; the KDoc above the function already says it is for tests. That removes both JVM references from PluginBase. It still does not move, and I tried: making its rh a TextResolver breaks 30 plugin files that call rh.gs(R.string.x) through the inherited property. That is churn in plugin and pump code which is not going multiplatform, so it waits for the ResourceHelper migration rather than being forced now. PluginDescription is behind PluginBase either way - it takes a (PluginBase) -> Any provider. --- .../kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt index bfd9d7b4438c..49293192de01 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.jetbrains.annotations.TestOnly /** * Created by mike on 09.06.2016. @@ -37,7 +36,10 @@ abstract class PluginBase( * the class simple name — matching the legacy `RunningConfiguration` encoding, so dual-write stays * consistent. Override to decouple from the class name (survive rename / R8) when a durable id is needed. */ - open val pluginId: String get() = javaClass.simpleName + // this::class.simpleName rather than javaClass.simpleName, so this file is not tied to the JVM. It + // gives the same string: a plugin is always a named class. simpleName is only null for an anonymous + // one, which would be a bug worth failing on rather than syncing an empty id. + open val pluginId: String get() = this::class.simpleName!! //only if translation exists // use long name as fallback @@ -118,7 +120,6 @@ abstract class PluginBase( * Version of setPluginEnabled used for testing only. * OnStart/OnStop is called directly. */ - @TestOnly fun setPluginEnabledBlocking(type: PluginType, newState: Boolean) { if (type == pluginDescription.mainType) { if (newState) { // enabling plugin From 450a3e6f7a0821e751956abf82ee8d3b24696041 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 16 Aug 2026 21:56:51 +0200 Subject: [PATCH 114/146] PluginDescription names plugins with TextRef pluginName, shortName and description were @StringRes ids with -1 meaning unset. They are TextRef? now, so a plugin names itself with something that carries no Android resource id, and "unset" is null rather than a sentinel. 64 files pass TextRef.AndroidRes(R.string.x) at the builder. That is fine from a plain Android module: TextRef lives in :core:keys commonMain, and a KMP module publishes an ordinary Android artifact, so pump drivers and plugins use it with no KMP plugin of their own - :pump:danar already had 43 uses before this. PreferenceSubScreenDef gains a TextRef primary constructor and keeps the resource id one as a secondary, so its 285 call sites are untouched while the three BG source plugins can pass a plugin's own name straight through. It already derived `title: TextRef` from the id internally, so this only exposes what it was doing. Readers lose their sentinel checks: SearchIndexBuilder compares against null, SearchableItem and QuickLaunchResolver pass the ref along instead of rewrapping it, and ConfigBuilderImpl uses PluginBase.name where it wanted the localized form. PluginBase keeps rh: ResourceHelper. Narrowing it to TextResolver is what would let PluginBase and PluginDescription reach commonMain, but it breaks plugins that hand the inherited rh to other ResourceHelper-typed APIs - VirtualPumpPlugin does this five times - so that belongs with the ResourceHelper migration, not here. --- .../aps/openAPSAMA/TestOpenAPSAMAPlugin.kt | 7 ++++--- .../aps/openAPSSMB/TestOpenAPSSMBPlugin.kt | 7 ++++--- .../TestOpenAPSSMBDynamicISFPlugin.kt | 6 +++--- .../aaps/core/interfaces/plugin/PluginBase.kt | 8 ++++---- .../interfaces/plugin/PluginDescription.kt | 13 +++++++------ .../pump/defs/PluginDescriptionTest.kt | 9 +++++---- .../preference/PreferenceSubScreenDef.kt | 19 ++++++++++++------- .../app/aaps/core/ui/search/SearchableItem.kt | 4 ++-- .../plugins/aps/autotune/AutotunePlugin.kt | 7 ++++--- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 7 ++++--- .../aps/openAPSAMA/OpenAPSAMAPlugin.kt | 7 ++++--- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 6 +++--- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 6 +++--- .../aaps/plugins/aps/loop/LoopPluginTest.kt | 5 +++-- .../calibration/LinearCalibrationPlugin.kt | 6 +++--- .../calibration/NoCalibrationPlugin.kt | 7 ++++--- .../configBuilder/ConfigBuilderImpl.kt | 8 ++++---- .../bgQualityCheck/BgQualityCheckPlugin.kt | 3 ++- .../constraints/dstHelper/DstHelperPlugin.kt | 2 +- .../objectives/ObjectivesPlugin.kt | 7 ++++--- .../constraints/safety/SafetyPlugin.kt | 2 +- .../SignatureVerifierPlugin.kt | 2 +- .../storage/StorageConstraintPlugin.kt | 2 +- .../versionChecker/VersionCheckerPlugin.kt | 3 ++- .../PersistentNotificationPlugin.kt | 5 +++-- .../IobCobCalculatorPlugin.kt | 3 ++- .../sensitivity/SensitivityAAPSPlugin.kt | 7 ++++--- .../sensitivity/SensitivityOref1Plugin.kt | 7 ++++--- .../SensitivityWeightedAveragePlugin.kt | 7 ++++--- .../plugins/smoothing/AvgSmoothingPlugin.kt | 7 ++++--- .../smoothing/ExponentialSmoothingPlugin.kt | 7 ++++--- .../plugins/smoothing/NoSmoothingPlugin.kt | 7 ++++--- .../smoothing/UnscentedKalmanFilterPlugin.kt | 7 ++++--- .../plugins/source/AbstractBgSourcePlugin.kt | 3 ++- ...stractBgSourceWithSensorInsertLogPlugin.kt | 3 ++- .../app/aaps/plugins/source/AidexPlugin.kt | 6 +++--- .../app/aaps/plugins/source/DexcomPlugin.kt | 6 +++--- .../app/aaps/plugins/source/GlimpPlugin.kt | 5 +++-- .../app/aaps/plugins/source/GlunovoPlugin.kt | 7 ++++--- .../aaps/plugins/source/IntelligoPlugin.kt | 7 ++++--- .../app/aaps/plugins/source/MM640gPlugin.kt | 5 +++-- .../plugins/source/NSClientSourcePlugin.kt | 7 ++++--- .../source/NotificationReaderPlugin.kt | 4 ++-- .../aaps/plugins/source/PatchedSiAppPlugin.kt | 5 +++-- .../plugins/source/PatchedSinoAppPlugin.kt | 5 +++-- .../app/aaps/plugins/source/PoctechPlugin.kt | 5 +++-- .../app/aaps/plugins/source/RandomBgPlugin.kt | 7 ++++--- .../app/aaps/plugins/source/SyaiPlugin.kt | 5 +++-- .../app/aaps/plugins/source/TomatoPlugin.kt | 7 ++++--- .../aaps/plugins/source/XdripSourcePlugin.kt | 5 +++-- .../plugins/source/instara/InstaraPlugin.kt | 8 +++++--- .../aaps/plugins/sync/garmin/GarminPlugin.kt | 7 ++++--- .../sync/nsclientV3/NSClientV3Plugin.kt | 7 ++++--- .../openhumans/OpenHumansUploaderPlugin.kt | 6 +++--- .../smsCommunicator/SmsCommunicatorPlugin.kt | 6 +++--- .../plugins/sync/tidepool/TidepoolPlugin.kt | 7 ++++--- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 7 ++++--- .../app/aaps/plugins/sync/wear/WearPlugin.kt | 7 ++++--- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 7 ++++--- .../nightscout/pump/combov2/ComboV2Plugin.kt | 6 +++--- .../aaps/pump/danar/AbstractDanaRPlugin.kt | 7 ++++--- .../pump/danarkorean/DanaRKoreanPlugin.kt | 3 ++- .../app/aaps/pump/danarv2/DanaRv2Plugin.kt | 3 ++- .../app/aaps/pump/danars/DanaRSPlugin.kt | 7 ++++--- .../app/aaps/pump/diaconn/DiaconnG8Plugin.kt | 7 ++++--- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 6 +++--- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 7 ++++--- .../app/aaps/pump/insight/InsightPlugin.kt | 6 +++--- .../pump/medtronic/MedtronicPumpPlugin.kt | 6 +++--- .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 6 +++--- .../omnipod/dash/OmnipodDashPumpPlugin.kt | 6 +++--- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 6 +++--- .../aaps/pump/virtual/VirtualPumpPlugin.kt | 6 +++--- .../aaps/ui/compose/main/MainNavigationBar.kt | 7 ++++--- .../preferences/PreferenceScreenView.kt | 10 +++++----- .../quickLaunch/QuickLaunchResolver.kt | 8 ++++---- .../app/aaps/ui/search/SearchIndexBuilder.kt | 4 ++-- 77 files changed, 265 insertions(+), 210 deletions(-) diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt index f2c174e2f59d..a1e84e1452ce 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.aps.openAPSAMA +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.aps.APS import app.aaps.core.interfaces.aps.APSResult @@ -75,10 +76,10 @@ class TestOpenAPSAMAPlugin @Inject constructor( ) : PluginBase( PluginDescription() .mainType(PluginType.APS) - .pluginName(R.string.openapsama) - .shortName(R.string.oaps_shortname) + .pluginName(TextRef.AndroidRes(R.string.openapsama)) + .shortName(TextRef.AndroidRes(R.string.oaps_shortname)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_ama), + .description(TextRef.AndroidRes(R.string.description_ama)), aapsLogger, rh ), APS, PluginConstraints { diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt index 14b4ff43c824..4a88ba986ebd 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.aps.openAPSSMB import android.content.Context +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.aps.SMBDefaults import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.aps.APS @@ -72,10 +73,10 @@ open class TestOpenAPSSMBPlugin @Inject constructor( ) : PluginBase( PluginDescription() .mainType(PluginType.APS) - .pluginName(R.string.openapssmb) - .shortName(app.aaps.core.ui.R.string.smb_shortname) + .pluginName(TextRef.AndroidRes(R.string.openapssmb)) + .shortName(TextRef.AndroidRes(app.aaps.core.ui.R.string.smb_shortname)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_smb) + .description(TextRef.AndroidRes(R.string.description_smb)) .setDefault(), aapsLogger, rh ), APS, PluginConstraints { diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt index 80abb3bcd54d..fd699448f7d7 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/TestOpenAPSSMBDynamicISFPlugin.kt @@ -82,9 +82,9 @@ class TestOpenAPSSMBDynamicISFPlugin @Inject constructor( init { pluginDescription - .pluginName(R.string.openaps_smb_dynamic_isf) - .description(R.string.description_smb_dynamic_isf) - .shortName(R.string.dynisf_shortname) + .pluginName(TextRef.AndroidRes(R.string.openaps_smb_dynamic_isf)) + .description(TextRef.AndroidRes(R.string.description_smb_dynamic_isf)) + .shortName(TextRef.AndroidRes(R.string.dynisf_shortname)) .preferencesVisibleInSimpleMode(true) .setDefault(false) } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt index 49293192de01..3c97816a010c 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt @@ -29,7 +29,7 @@ abstract class PluginBase( private var state = State.NOT_INITIALIZED open val name: String - get() = if (pluginDescription.pluginName == -1) "UNKNOWN" else rh.gs(pluginDescription.pluginName) + get() = pluginDescription.pluginName?.let { rh.gs(it) } ?: "UNKNOWN" /** * Stable identity for syncing the active-plugin selection (see the `ActivePlugin*` keys). Defaults to @@ -45,14 +45,14 @@ abstract class PluginBase( // use long name as fallback val nameShort: String get() { - if (pluginDescription.shortName == -1) return name - val translatedName = rh.gs(pluginDescription.shortName) + val shortNameRef = pluginDescription.shortName ?: return name + val translatedName = rh.gs(shortNameRef) return if (translatedName.trim { it <= ' ' }.isNotEmpty()) translatedName else name // use long name as fallback } val description: String? - get() = if (pluginDescription.description == -1) null else rh.gs(pluginDescription.description) + get() = pluginDescription.description?.let { rh.gs(it) } fun getType(): PluginType = pluginDescription.mainType diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt index fc221370e929..f88972065ba1 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt @@ -2,6 +2,7 @@ package app.aaps.core.interfaces.plugin import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.data.plugin.PluginType +import app.aaps.core.keys.interfaces.TextRef open class PluginDescription { @@ -20,9 +21,9 @@ open class PluginDescription { var neverVisible = false var alwaysEnabled = false var showInList = { true } - var pluginName = -1 - var shortName = -1 - var description = -1 + var pluginName: TextRef? = null + var shortName: TextRef? = null + var description: TextRef? = null var enableByDefault = false var defaultPlugin = false @@ -35,10 +36,10 @@ open class PluginDescription { fun showInList(showInList: () -> Boolean): PluginDescription = this.also { it.showInList = showInList } fun icon(icon: ImageVector): PluginDescription = this.also { it.icon = icon } - fun pluginName(pluginName: Int): PluginDescription = this.also { it.pluginName = pluginName } - fun shortName(shortName: Int): PluginDescription = this.also { it.shortName = shortName } + fun pluginName(pluginName: TextRef): PluginDescription = this.also { it.pluginName = pluginName } + fun shortName(shortName: TextRef): PluginDescription = this.also { it.shortName = shortName } fun enableByDefault(enableByDefault: Boolean): PluginDescription = this.also { it.enableByDefault = enableByDefault } - fun description(description: Int): PluginDescription = this.also { it.description = description } + fun description(description: TextRef): PluginDescription = this.also { it.description = description } fun setDefault(value: Boolean = true): PluginDescription = this.also { it.defaultPlugin = value } fun preferencesVisibleInSimpleMode(value: Boolean): PluginDescription = this.also { it.preferencesVisibleInSimpleMode = value } fun composeContent(provider: (PluginBase) -> Any): PluginDescription = this.also { it.composeContentProvider = provider } diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/pump/defs/PluginDescriptionTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/pump/defs/PluginDescriptionTest.kt index d5de6971a812..bd27b55f2821 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/pump/defs/PluginDescriptionTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/interfaces/pump/defs/PluginDescriptionTest.kt @@ -1,5 +1,6 @@ package app.aaps.core.objects.interfaces.pump.defs +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.plugin.PluginDescription import com.google.common.truth.Truth.assertThat @@ -28,13 +29,13 @@ class PluginDescriptionTest { } @Test fun pluginName() { - val pluginDescription = PluginDescription().pluginName(10) - assertThat(pluginDescription.pluginName.toLong()).isEqualTo(10) + val ref = TextRef.AndroidRes(10) + assertThat(PluginDescription().pluginName(ref).pluginName).isEqualTo(ref) } @Test fun shortNameTest() { - val pluginDescription = PluginDescription().shortName(10) - assertThat(pluginDescription.shortName.toLong()).isEqualTo(10) + val ref = TextRef.AndroidRes(10) + assertThat(PluginDescription().shortName(ref).shortName).isEqualTo(ref) } @Test fun enableByDefault() { diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt index 766d0bd343a5..fb0a005b7f26 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt @@ -23,17 +23,22 @@ import app.aaps.core.keys.interfaces.TextRef */ data class PreferenceSubScreenDef( val key: String, - val titleResId: Int, + /** Screen title, in the same form as [PreferenceKey.title]. */ + val title: TextRef, val items: List = emptyList(), - val summaryResId: Int? = null, + /** Optional summary, in the same form as [PreferenceKey.summary]. */ + val summary: TextRef? = null, val icon: ImageVector? = null ) : PreferenceItem { - /** Screen title, in the same form as [PreferenceKey.title]. */ - val title: TextRef = TextRef.AndroidRes(titleResId) - - /** Optional summary, in the same form as [PreferenceKey.summary]. */ - val summary: TextRef? = summaryResId?.let { TextRef.AndroidRes(it) } + /** Resource id form, for the many call sites that still name their strings with R.string. */ + constructor( + key: String, + titleResId: Int, + items: List = emptyList(), + summaryResId: Int? = null, + icon: ImageVector? = null + ) : this(key, TextRef.AndroidRes(titleResId), items, summaryResId?.let { TextRef.AndroidRes(it) }, icon) /** Titles of the contained items, used to build the summary line in the parent list. */ fun effectiveSummaryItems(): List = diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt index 7c912690b7ef..7e7e71b61f68 100644 --- a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt +++ b/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt @@ -111,8 +111,8 @@ sealed class SearchableItem { ) : SearchableItem() { override val key: String = pluginRef.javaClass.simpleName - override val title: TextRef = TextRef.AndroidRes(pluginRef.pluginDescription.pluginName) - override val summary: TextRef? = pluginRef.pluginDescription.description.takeIf { it != -1 }?.let { TextRef.AndroidRes(it) } + override val title: TextRef = pluginRef.pluginDescription.pluginName ?: TextRef.Literal(pluginRef.pluginId) + override val summary: TextRef? = pluginRef.pluginDescription.description override val plugin: PluginBase = pluginRef } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index 94887961d363..93746ae67add 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -3,6 +3,7 @@ package app.aaps.plugins.aps.autotune import android.view.View +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.ICfg import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.time.T @@ -81,8 +82,8 @@ class AutotunePlugin @Inject constructor( pluginDescription = PluginDescription() .mainType(PluginType.GENERAL) .icon(IcPluginAutotune) - .pluginName(app.aaps.core.ui.R.string.autotune) - .shortName(R.string.autotune_shortname) + .pluginName(TextRef.AndroidRes(app.aaps.core.ui.R.string.autotune)) + .shortName(TextRef.AndroidRes(R.string.autotune_shortname)) .composeContent { plugin -> AutotuneComposeContent( autotunePlugin = plugin as AutotunePlugin, @@ -101,7 +102,7 @@ class AutotunePlugin @Inject constructor( ) } .showInList { config.isEngineeringMode() && config.isDev() || config.isEnabled(ExternalOptions.ENABLE_AUTOTUNE) } - .description(R.string.autotune_description), + .description(TextRef.AndroidRes(R.string.autotune_description)), ownPreferences = AutotuneStringKey.entries, aapsLogger, rh, preferences ), Autotune { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index 9c13becfc114..8e32ff4b0f10 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -11,6 +11,7 @@ import android.os.Handler import android.os.HandlerThread import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationCompat +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.BS import app.aaps.core.data.model.DS @@ -144,10 +145,10 @@ class LoopPlugin @Inject constructor( ) } .icon(IcLoopClosed) - .pluginName(app.aaps.core.ui.R.string.loop) - .shortName(R.string.loop_shortname) + .pluginName(TextRef.AndroidRes(app.aaps.core.ui.R.string.loop)) + .shortName(TextRef.AndroidRes(R.string.loop_shortname)) .alwaysEnabled(config.APS) - .description(R.string.description_loop), + .description(TextRef.AndroidRes(R.string.description_loop)), aapsLogger, rh ), Loop, PluginConstraints { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt index 228400b88ffe..569a623889fe 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.aps.openAPSAMA +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.aps.SMBDefaults import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.plugin.PluginType @@ -89,11 +90,11 @@ class OpenAPSAMAPlugin @Inject constructor( ) } .icon(IcPluginOpenAPS) - .pluginName(R.string.openapsama) - .shortName(R.string.oaps_shortname) + .pluginName(TextRef.AndroidRes(R.string.openapsama)) + .shortName(TextRef.AndroidRes(R.string.oaps_shortname)) .preferencesVisibleInSimpleMode(false) .showInList { config.APS || config.AAPSCLIENT } // AAPSCLIENT: visible so a client can select the master's APS - .description(R.string.description_ama), + .description(TextRef.AndroidRes(R.string.description_ama)), ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences ), APS, PluginConstraints { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt index 469c7e7fecbd..d70a9461f91d 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt @@ -108,11 +108,11 @@ open class OpenAPSAutoISFPlugin @Inject constructor( ) } .icon(IcPluginOpenAPS) - .pluginName(R.string.openaps_auto_isf) - .shortName(R.string.autoisf_shortname) + .pluginName(TextRef.AndroidRes(R.string.openaps_auto_isf)) + .shortName(TextRef.AndroidRes(R.string.autoisf_shortname)) .preferencesVisibleInSimpleMode(false) .showInList { (config.APS || config.AAPSCLIENT) && config.isEngineeringMode() && config.isDev() } // AAPSCLIENT: visible so a client can select the master's APS (still eng+dev only) - .description(R.string.description_auto_isf), + .description(TextRef.AndroidRes(R.string.description_auto_isf)), ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences ), APS, PluginConstraints { diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index b07e7ce15460..5e0149604330 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -110,11 +110,11 @@ open class OpenAPSSMBPlugin @Inject constructor( ) } .icon(IcPluginOpenAPS) - .pluginName(R.string.openapssmb) - .shortName(app.aaps.core.ui.R.string.smb_shortname) + .pluginName(TextRef.AndroidRes(R.string.openapssmb)) + .shortName(TextRef.AndroidRes(app.aaps.core.ui.R.string.smb_shortname)) .preferencesVisibleInSimpleMode(false) .showInList { config.APS || config.AAPSCLIENT } // AAPSCLIENT: visible so a client can select the master's APS - .description(R.string.description_smb) + .description(TextRef.AndroidRes(R.string.description_smb)) .setDefault(), ownPreferences = ApsIntentKey.entries, aapsLogger, rh, preferences diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt index df415dcd333e..481c155d91ac 100644 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt +++ b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/loop/LoopPluginTest.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.aps.loop import android.app.NotificationManager import android.content.Context +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.DS import app.aaps.core.data.model.RM import app.aaps.core.data.plugin.PluginType @@ -79,8 +80,8 @@ class LoopPluginTest : TestBaseWithProfile() { @Test fun testPluginInterface() { - whenever(rh.gs(app.aaps.core.ui.R.string.loop)).thenReturn("Loop") - whenever(rh.gs(app.aaps.plugins.aps.R.string.loop_shortname)).thenReturn("LOOP") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.loop))).thenReturn("Loop") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.plugins.aps.R.string.loop_shortname))).thenReturn("LOOP") // whenever(preferences.get(StringKey.LoopApsMode)).thenReturn(ApsMode.CLOSED.name) val pumpDescription = PumpDescription() whenever(virtualPumpPlugin.pumpDescription).thenReturn(pumpDescription) diff --git a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt index c0f19f2e5c79..1bdebe5a4d21 100644 --- a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt +++ b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt @@ -52,9 +52,9 @@ class LinearCalibrationPlugin @Inject constructor( PluginDescription() .mainType(PluginType.CALIBRATION) .icon(IcCalibration) - .pluginName(R.string.linear_calibration_name) - .shortName(R.string.calibration_shortname) - .description(R.string.description_linear_calibration) + .pluginName(TextRef.AndroidRes(R.string.linear_calibration_name)) + .shortName(TextRef.AndroidRes(R.string.calibration_shortname)) + .description(TextRef.AndroidRes(R.string.description_linear_calibration)) .composeContent { CalibrationComposeContent() }, aapsLogger, rh ), Calibration { diff --git a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/NoCalibrationPlugin.kt b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/NoCalibrationPlugin.kt index 9f3485c3c308..80eb8647133a 100644 --- a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/NoCalibrationPlugin.kt +++ b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/NoCalibrationPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.calibration +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.calibration.AddEntryResult @@ -22,9 +23,9 @@ class NoCalibrationPlugin @Inject constructor( .mainType(PluginType.CALIBRATION) .icon(IcCalibration) .setDefault(true) - .pluginName(R.string.no_calibration_name) - .shortName(R.string.calibration_shortname) - .description(R.string.description_no_calibration), + .pluginName(TextRef.AndroidRes(R.string.no_calibration_name)) + .shortName(TextRef.AndroidRes(R.string.calibration_shortname)) + .description(TextRef.AndroidRes(R.string.description_no_calibration)), aapsLogger, rh ), Calibration { diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt index 24510bfb7c0a..35b97f9df094 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt @@ -221,8 +221,8 @@ class ConfigBuilderImpl @Inject constructor( uel.log( action = Action.HW_PUMP_ALLOWED, source = Sources.ConfigBuilder, - note = rh.gs(plugin.pluginDescription.pluginName), - value = ValueWithUnit.SimpleString(rh.gsNotLocalised(plugin.pluginDescription.pluginName)) + note = plugin.name, + value = ValueWithUnit.SimpleString(plugin.pluginDescription.pluginName?.let { rh.gsNotLocalised(it) } ?: plugin.pluginId) ) } aapsLogger.debug(LTag.PUMP, "First time HW pump allowed!") @@ -234,14 +234,14 @@ class ConfigBuilderImpl @Inject constructor( scope.launch { uel.log( Action.PLUGIN_ENABLED, Sources.ConfigBuilder, null, - ValueWithUnit.SimpleString(rh.gsNotLocalised(changedPlugin.pluginDescription.pluginName)) + ValueWithUnit.SimpleString(changedPlugin.pluginDescription.pluginName?.let { rh.gsNotLocalised(it) } ?: changedPlugin.pluginId) ) } } else if (!enabled) { scope.launch { uel.log( Action.PLUGIN_DISABLED, Sources.ConfigBuilder, null, - ValueWithUnit.SimpleString(rh.gsNotLocalised(changedPlugin.pluginDescription.pluginName)) + ValueWithUnit.SimpleString(changedPlugin.pluginDescription.pluginName?.let { rh.gsNotLocalised(it) } ?: changedPlugin.pluginId) ) } } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt index 9433fbf1dad6..efb3eca67d51 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.constraints.bgQualityCheck +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.time.T import app.aaps.core.interfaces.bgQualityCheck.BgQualityCheck @@ -42,7 +43,7 @@ class BgQualityCheckPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.bg_quality), + .pluginName(TextRef.AndroidRes(R.string.bg_quality)), aapsLogger, rh ), PluginConstraints, BgQualityCheck { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt index 49b65db2c300..8606f2acd39a 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/dstHelper/DstHelperPlugin.kt @@ -40,7 +40,7 @@ class DstHelperPlugin @Inject constructor( .mainType(PluginType.GENERAL) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.dst_plugin_name), + .pluginName(TextRef.AndroidRes(R.string.dst_plugin_name)), ownPreferences = DstHelperLongKey.entries, aapsLogger, rh, preferences ), DstHelper { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt index 42ddfbe69c09..b3e64f3c42b7 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.constraints.objectives +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.Constraint @@ -40,10 +41,10 @@ class ObjectivesPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .composeContent { ObjectivesComposeContent() } .icon(IcPluginObjectives) - .pluginName(app.aaps.core.ui.R.string.objectives) - .shortName(R.string.objectives_shortname) + .pluginName(TextRef.AndroidRes(app.aaps.core.ui.R.string.objectives)) + .shortName(TextRef.AndroidRes(R.string.objectives_shortname)) .enableByDefault(config.APS) - .description(R.string.description_objectives), + .description(TextRef.AndroidRes(R.string.description_objectives)), ownPreferences = ObjectivesBooleanComposedKey.entries + ObjectivesLongComposedKey.entries, aapsLogger, rh, preferences ), PluginConstraints, Objectives { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt index 06f4c8a8ca2d..7414e15e332f 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt @@ -54,7 +54,7 @@ class SafetyPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.safety) + .pluginName(TextRef.AndroidRes(R.string.safety)) .icon(Icons.Default.Shield), aapsLogger, rh ), PluginConstraints, Safety { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt index e5557a9e377b..c9ec1e488379 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/signatureVerifier/SignatureVerifierPlugin.kt @@ -51,7 +51,7 @@ class SignatureVerifierPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.signature_verifier), + .pluginName(TextRef.AndroidRes(R.string.signature_verifier)), ownPreferences = SignatureVerifierLongKey.entries, aapsLogger, rh, preferences ), PluginConstraints { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt index 58a4f18692ff..d2235bbc6cff 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt @@ -30,7 +30,7 @@ class StorageConstraintPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.storage), + .pluginName(TextRef.AndroidRes(R.string.storage)), aapsLogger, rh ), PluginConstraints { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt index 2b2dde0a397d..453476299edf 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.constraints.versionChecker +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.constraints.Constraint @@ -30,7 +31,7 @@ class VersionCheckerPlugin @Inject constructor( .mainType(PluginType.CONSTRAINTS) .alwaysEnabled(true) .showInList { false } - .pluginName(R.string.version_checker), + .pluginName(TextRef.AndroidRes(R.string.version_checker)), ownPreferences = VersionCheckerLongKey.entries, aapsLogger, rh, preferences ), PluginConstraints { diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index 4d975a1d1c21..03f88f8055d5 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.graphics.BitmapFactory import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TrendArrow import app.aaps.core.data.plugin.PluginType @@ -84,11 +85,11 @@ class PersistentNotificationPlugin @Inject constructor( ) : PluginBase( PluginDescription() .mainType(PluginType.GENERAL) - .pluginName(R.string.ongoingnotificaction) + .pluginName(TextRef.AndroidRes(R.string.ongoingnotificaction)) .enableByDefault(true) .alwaysEnabled(true) .showInList { false } - .description(R.string.description_persistent_notification), + .description(TextRef.AndroidRes(R.string.description_persistent_notification)), aapsLogger, rh ) { diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt index b24a4f5ebc62..1e432326e517 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/iob/iobCobCalculator/IobCobCalculatorPlugin.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.main.iob.iobCobCalculator import androidx.collection.LongSparseArray +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.aps.BasalData import app.aaps.core.data.configuration.Constants import app.aaps.core.data.iob.CobInfo @@ -96,7 +97,7 @@ class IobCobCalculatorPlugin @Inject constructor( ) : PluginBase( PluginDescription() .mainType(PluginType.GENERAL) - .pluginName(R.string.iob_cob_calculator) + .pluginName(TextRef.AndroidRes(R.string.iob_cob_calculator)) .showInList { false } .alwaysEnabled(true), aapsLogger, rh diff --git a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityAAPSPlugin.kt b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityAAPSPlugin.kt index 0994140cb251..76675ef67f78 100644 --- a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityAAPSPlugin.kt +++ b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityAAPSPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sensitivity +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.PS import app.aaps.core.data.model.TE import app.aaps.core.data.plugin.PluginType @@ -38,9 +39,9 @@ class SensitivityAAPSPlugin @Inject constructor( PluginDescription() .mainType(PluginType.SENSITIVITY) .icon(IcAs) - .pluginName(R.string.sensitivity_aaps) - .shortName(R.string.sensitivity_plugin_shortname) - .description(R.string.description_sensitivity_aaps), + .pluginName(TextRef.AndroidRes(R.string.sensitivity_aaps)) + .shortName(TextRef.AndroidRes(R.string.sensitivity_plugin_shortname)) + .description(TextRef.AndroidRes(R.string.description_sensitivity_aaps)), aapsLogger, rh, preferences ) { diff --git a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt index aab312c2cfa2..ea2f9eb852a7 100644 --- a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt +++ b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sensitivity +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.PS import app.aaps.core.data.model.TE import app.aaps.core.data.plugin.PluginType @@ -36,10 +37,10 @@ class SensitivityOref1Plugin @Inject constructor( PluginDescription() .mainType(PluginType.SENSITIVITY) .icon(IcAs) - .pluginName(R.string.sensitivity_oref1) - .shortName(R.string.sensitivity_plugin_shortname) + .pluginName(TextRef.AndroidRes(R.string.sensitivity_oref1)) + .shortName(TextRef.AndroidRes(R.string.sensitivity_plugin_shortname)) .enableByDefault(true) - .description(R.string.description_sensitivity_oref1) + .description(TextRef.AndroidRes(R.string.description_sensitivity_oref1)) .setDefault(), aapsLogger, rh, preferences ), PluginConstraints { diff --git a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityWeightedAveragePlugin.kt b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityWeightedAveragePlugin.kt index 2b0b586f65e9..b1ed21ea9cf9 100644 --- a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityWeightedAveragePlugin.kt +++ b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityWeightedAveragePlugin.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.sensitivity import androidx.collection.LongSparseArray +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.PS import app.aaps.core.data.model.TE import app.aaps.core.data.plugin.PluginType @@ -38,9 +39,9 @@ class SensitivityWeightedAveragePlugin @Inject constructor( PluginDescription() .mainType(PluginType.SENSITIVITY) .icon(IcAs) - .pluginName(R.string.sensitivity_weighted_average) - .shortName(R.string.sensitivity_plugin_shortname) - .description(R.string.description_sensitivity_weighted_average), + .pluginName(TextRef.AndroidRes(R.string.sensitivity_weighted_average)) + .shortName(TextRef.AndroidRes(R.string.sensitivity_plugin_shortname)) + .description(TextRef.AndroidRes(R.string.description_sensitivity_weighted_average)), aapsLogger, rh, preferences ) { diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/AvgSmoothingPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/AvgSmoothingPlugin.kt index 40e4dec73e24..73108d1de3cf 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/AvgSmoothingPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/AvgSmoothingPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.smoothing import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Timeline +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.model.TrendArrow import app.aaps.core.data.plugin.PluginType @@ -24,9 +25,9 @@ class AvgSmoothingPlugin @Inject constructor( PluginDescription() .mainType(PluginType.SMOOTHING) .icon(Icons.Default.Timeline) - .pluginName(R.string.avg_smoothing_name) - .shortName(R.string.smoothing_shortname) - .description(R.string.description_avg_smoothing), + .pluginName(TextRef.AndroidRes(R.string.avg_smoothing_name)) + .shortName(TextRef.AndroidRes(R.string.smoothing_shortname)) + .description(TextRef.AndroidRes(R.string.description_avg_smoothing)), aapsLogger, rh ), Smoothing { diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/ExponentialSmoothingPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/ExponentialSmoothingPlugin.kt index e5384885921b..86e8b47173d3 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/ExponentialSmoothingPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/ExponentialSmoothingPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.smoothing import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Timeline +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.model.TrendArrow import app.aaps.core.data.plugin.PluginType @@ -23,9 +24,9 @@ class ExponentialSmoothingPlugin @Inject constructor( PluginDescription() .mainType(PluginType.SMOOTHING) .icon(Icons.Default.Timeline) - .pluginName(R.string.exponential_smoothing_name) - .shortName(R.string.smoothing_shortname) - .description(R.string.description_exponential_smoothing), + .pluginName(TextRef.AndroidRes(R.string.exponential_smoothing_name)) + .shortName(TextRef.AndroidRes(R.string.smoothing_shortname)) + .description(TextRef.AndroidRes(R.string.description_exponential_smoothing)), aapsLogger, rh ), Smoothing { diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/NoSmoothingPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/NoSmoothingPlugin.kt index 1aad9e69e472..9ca1984cf62d 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/NoSmoothingPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/NoSmoothingPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.smoothing import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Timeline +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.AAPSLogger @@ -21,9 +22,9 @@ class NoSmoothingPlugin @Inject constructor( .mainType(PluginType.SMOOTHING) .icon(Icons.Default.Timeline) .setDefault(true) - .pluginName(R.string.no_smoothing_name) - .shortName(R.string.smoothing_shortname) - .description(R.string.description_no_smoothing), + .pluginName(TextRef.AndroidRes(R.string.no_smoothing_name)) + .shortName(TextRef.AndroidRes(R.string.smoothing_shortname)) + .description(TextRef.AndroidRes(R.string.description_no_smoothing)), aapsLogger, rh ), Smoothing { diff --git a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt index 6deca1757709..b30f6fcc14ec 100644 --- a/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt +++ b/plugins/smoothing/src/main/kotlin/app/aaps/plugins/smoothing/UnscentedKalmanFilterPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.smoothing import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Timeline +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.model.TE import app.aaps.core.data.model.TrendArrow @@ -68,9 +69,9 @@ class UnscentedKalmanFilterPlugin @Inject constructor( pluginDescription = PluginDescription() .mainType(PluginType.SMOOTHING) .icon(Icons.Default.Timeline) - .pluginName(R.string.UKF_name) - .shortName(R.string.smoothing_shortname) - .description(R.string.description_UKF), + .pluginName(TextRef.AndroidRes(R.string.UKF_name)) + .shortName(TextRef.AndroidRes(R.string.smoothing_shortname)) + .description(TextRef.AndroidRes(R.string.description_UKF)), ownPreferences = UkfLongNonKey.entries + UkfIntNonKey.entries + UkfDoubleNonKey.entries, aapsLogger, rh, preferences ), Smoothing { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt index 2a6e716d8a55..ea6f80e36205 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourcePlugin.kt @@ -22,7 +22,8 @@ abstract class AbstractBgSourcePlugin( override fun getPreferenceScreenContent() = PreferenceSubScreenDef( key = "bg_source_settings", - titleResId = pluginDescription.pluginName, + // a BG source plugin always names itself + title = pluginDescription.pluginName!!, items = listOf( BooleanKey.BgSourceUploadToNs diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt index ed5129a6d2a0..55c04967f37f 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AbstractBgSourceWithSensorInsertLogPlugin.kt @@ -20,7 +20,8 @@ abstract class AbstractBgSourceWithSensorInsertLogPlugin( override fun getPreferenceScreenContent() = PreferenceSubScreenDef( key = "bg_source_with_sensor_settings", - titleResId = pluginDescription.pluginName, + // a BG source plugin always names itself + title = pluginDescription.pluginName!!, items = listOf( BooleanKey.BgSourceUploadToNs, BooleanKey.BgSourceCreateSensorChange diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt index 9d28956cf2f9..6686e5cf3eaf 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/AidexPlugin.kt @@ -53,10 +53,10 @@ class AidexPlugin @Inject constructor( ) } .icon(IcGenericCgm) - .pluginName(R.string.aidex) - .shortName(R.string.aidex_short) + .pluginName(TextRef.AndroidRes(R.string.aidex)) + .shortName(TextRef.AndroidRes(R.string.aidex_short)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_aidex), + .description(TextRef.AndroidRes(R.string.description_source_aidex)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt index 3cb7a4ac5033..02e194acc93c 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/DexcomPlugin.kt @@ -61,10 +61,10 @@ class DexcomPlugin @Inject constructor( ) } .icon(IcPluginByoda) - .pluginName(R.string.dexcom_app_patched) - .shortName(R.string.dexcom_short) + .pluginName(TextRef.AndroidRes(R.string.dexcom_app_patched)) + .shortName(TextRef.AndroidRes(R.string.dexcom_short)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_dexcom), + .description(TextRef.AndroidRes(R.string.description_source_dexcom)), aapsLogger = aapsLogger, rh = rh, preferences = preferences diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlimpPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlimpPlugin.kt index e9d93c413a62..ab6f17875974 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlimpPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlimpPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -43,9 +44,9 @@ class GlimpPlugin @Inject constructor( ) } .icon(IcPluginGlimp) - .pluginName(R.string.glimp) + .pluginName(TextRef.AndroidRes(R.string.glimp)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_glimp), + .description(TextRef.AndroidRes(R.string.description_source_glimp)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt index 5f5dac758035..fb887429921d 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/GlunovoPlugin.kt @@ -7,6 +7,7 @@ import android.os.Handler import android.os.HandlerThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit @@ -52,10 +53,10 @@ class GlunovoPlugin @Inject constructor( ) } .icon(IcPluginGlunovo) - .pluginName(R.string.glunovo) - .shortName(R.string.glunovo) + .pluginName(TextRef.AndroidRes(R.string.glunovo)) + .shortName(TextRef.AndroidRes(R.string.glunovo)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_glunovo), + .description(TextRef.AndroidRes(R.string.description_source_glunovo)), ownPreferences = GlunovoLongKey.entries, aapsLogger, resourceHelper, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt index 9aa2bad42b6f..a60701e50e67 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/IntelligoPlugin.kt @@ -7,6 +7,7 @@ import android.os.Handler import android.os.HandlerThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit @@ -52,10 +53,10 @@ class IntelligoPlugin @Inject constructor( ) } .icon(IcPluginIntelligo) - .pluginName(R.string.intelligo) - .shortName(R.string.intelligo) + .pluginName(TextRef.AndroidRes(R.string.intelligo)) + .shortName(TextRef.AndroidRes(R.string.intelligo)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_intelligo), + .description(TextRef.AndroidRes(R.string.description_source_intelligo)), ownPreferences = IntelligoLongKey.entries, aapsLogger, resourceHelper, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/MM640gPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/MM640gPlugin.kt index 19d1f4d2d04a..0941ed4c63ae 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/MM640gPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/MM640gPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -46,9 +47,9 @@ class MM640gPlugin @Inject constructor( ) } .icon(IcPluginMM640G) - .pluginName(R.string.mm640g) + .pluginName(TextRef.AndroidRes(R.string.mm640g)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_mm640g), + .description(TextRef.AndroidRes(R.string.description_source_mm640g)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt index 72ce05958e9e..64861d6333bd 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.source +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger @@ -27,9 +28,9 @@ class NSClientSourcePlugin @Inject constructor( ) } .icon(IcPluginNsClientBg) - .pluginName(R.string.ns_client_bg) - .shortName(R.string.ns_client_bg_short) - .description(R.string.description_source_ns_client) + .pluginName(TextRef.AndroidRes(R.string.ns_client_bg)) + .shortName(TextRef.AndroidRes(R.string.ns_client_bg_short)) + .description(TextRef.AndroidRes(R.string.description_source_ns_client)) .alwaysEnabled(config.AAPSCLIENT) .setDefault(config.AAPSCLIENT), aapsLogger, rh diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt index 6a60a0aec8df..f871b43bc96a 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NotificationReaderPlugin.kt @@ -35,9 +35,9 @@ class NotificationReaderPlugin @Inject constructor( ) } .icon(IcPluginByoda) - .pluginName(R.string.notification_reader) + .pluginName(TextRef.AndroidRes(R.string.notification_reader)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_notification_reader), + .description(TextRef.AndroidRes(R.string.description_source_notification_reader)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSiAppPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSiAppPlugin.kt index c587eefc4739..362ecef68f7b 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSiAppPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSiAppPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -45,9 +46,9 @@ class PatchedSiAppPlugin @Inject constructor( ) } .icon(IcGenericCgm) - .pluginName(R.string.patched_si_app) + .pluginName(TextRef.AndroidRes(R.string.patched_si_app)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_patched_si_app), + .description(TextRef.AndroidRes(R.string.description_source_patched_si_app)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSinoAppPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSinoAppPlugin.kt index af5a4aebdbc7..47a92d35b470 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSinoAppPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PatchedSinoAppPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -45,9 +46,9 @@ class PatchedSinoAppPlugin @Inject constructor( ) } .icon(IcGenericCgm) - .pluginName(R.string.patched_sino_app) + .pluginName(TextRef.AndroidRes(R.string.patched_sino_app)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_patched_sino_app), + .description(TextRef.AndroidRes(R.string.description_source_patched_sino_app)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PoctechPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PoctechPlugin.kt index f7df33d53260..184f18c65bc4 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/PoctechPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/PoctechPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit @@ -48,9 +49,9 @@ class PoctechPlugin @Inject constructor( ) } .icon(IcPluginPocTec) - .pluginName(R.string.poctech) + .pluginName(TextRef.AndroidRes(R.string.poctech)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_poctech), + .description(TextRef.AndroidRes(R.string.description_source_poctech)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/RandomBgPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/RandomBgPlugin.kt index 18fe82a28d35..68f53921c243 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/RandomBgPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/RandomBgPlugin.kt @@ -7,6 +7,7 @@ import android.os.HandlerThread import android.os.PowerManager import android.os.SystemClock import androidx.annotation.VisibleForTesting +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.CA import app.aaps.core.data.model.GV import app.aaps.core.data.model.IDs @@ -60,10 +61,10 @@ class RandomBgPlugin @Inject constructor( ) } .icon(IcPluginRandomBg) - .pluginName(R.string.random_bg) - .shortName(R.string.random_bg_short) + .pluginName(TextRef.AndroidRes(R.string.random_bg)) + .shortName(TextRef.AndroidRes(R.string.random_bg_short)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_random_bg), + .description(TextRef.AndroidRes(R.string.description_source_random_bg)), aapsLogger = aapsLogger, rh = rh, preferences = preferences, diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/SyaiPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/SyaiPlugin.kt index 698c135f75e4..6f4f1b07201d 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/SyaiPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/SyaiPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -45,9 +46,9 @@ class SyaiPlugin @Inject constructor( ) } .icon(IcPluginSyai) - .pluginName(R.string.syai_tag_app) + .pluginName(TextRef.AndroidRes(R.string.syai_tag_app)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_patched_syai_tag_app), + .description(TextRef.AndroidRes(R.string.description_source_patched_syai_tag_app)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/TomatoPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/TomatoPlugin.kt index 07e5f416c8ca..5aad863ad551 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/TomatoPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/TomatoPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow @@ -42,10 +43,10 @@ class TomatoPlugin @Inject constructor( ) } .icon(IcPluginTomato) - .pluginName(R.string.tomato) - .shortName(R.string.tomato_short) + .pluginName(TextRef.AndroidRes(R.string.tomato)) + .shortName(TextRef.AndroidRes(R.string.tomato_short)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_tomato), + .description(TextRef.AndroidRes(R.string.description_source_tomato)), ownPreferences = emptyList(), aapsLogger, rh, preferences, config ), BgSource { diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt index 1a489fc5cbc3..2ebc5a9cdfe6 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt @@ -6,6 +6,7 @@ import android.os.Bundle import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TE @@ -55,9 +56,9 @@ class XdripSourcePlugin @Inject constructor( ) } .icon(IcXDrip) - .pluginName(R.string.source_xdrip) + .pluginName(TextRef.AndroidRes(R.string.source_xdrip)) .preferencesVisibleInSimpleMode(false) - .description(R.string.description_source_xdrip), + .description(TextRef.AndroidRes(R.string.description_source_xdrip)), aapsLogger = aapsLogger, rh = rh, preferences = preferences diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt index 9e3240fb4d09..e5f34322090c 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/instara/InstaraPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GV import app.aaps.core.data.model.SourceSensor @@ -54,9 +55,9 @@ class InstaraPlugin @Inject constructor( ) } .icon(IcGenericCgm) - .pluginName(app.aaps.plugins.source.R.string.instara_app) + .pluginName(TextRef.AndroidRes(app.aaps.plugins.source.R.string.instara_app)) .preferencesVisibleInSimpleMode(false) - .description(app.aaps.plugins.source.R.string.description_source_instara_app), + .description(TextRef.AndroidRes(app.aaps.plugins.source.R.string.description_source_instara_app)), // Register Instara plugin-local preference/non-preference key enums ownPreferences = InstaraBooleanKey.entries + InstaraStringKey.entries, aapsLogger, rh, preferences, config @@ -67,7 +68,8 @@ class InstaraPlugin @Inject constructor( // Instara-specific setting in the current compose-based preference screen system override fun getPreferenceScreenContent() = PreferenceSubScreenDef( key = "bg_source_settings", - titleResId = pluginDescription.pluginName, + // a BG source plugin always names itself + title = pluginDescription.pluginName!!, items = listOf( BooleanKey.BgSourceUploadToNs, InstaraBooleanKey.HistoryRequestEnabled diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index bc63c53a3825..6f361dbb0d5b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.sync.garmin import android.content.Context import androidx.annotation.VisibleForTesting +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.plugin.PluginType @@ -61,9 +62,9 @@ class GarminPlugin @Inject constructor( pluginDescription = PluginDescription() .mainType(PluginType.SYNC) .icon(IcPluginGarmin) - .pluginName(R.string.garmin) - .shortName(R.string.garmin) - .description(R.string.garmin_description), + .pluginName(TextRef.AndroidRes(R.string.garmin)) + .shortName(TextRef.AndroidRes(R.string.garmin)) + .description(TextRef.AndroidRes(R.string.garmin_description)), ownPreferences = GarminStringKey.entries + GarminBooleanKey.entries + GarminIntKey.entries, aapsLogger, resourceHelper, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 1e20244eab6c..ebd5be421e5e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -13,6 +13,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.HR import app.aaps.core.data.model.HasIDs import app.aaps.core.data.model.SC @@ -155,9 +156,9 @@ class NSClientV3Plugin @Inject constructor( PluginDescription() .mainType(PluginType.SYNC) .icon(IcPluginNsClient) - .pluginName(R.string.ns_client_v3_title) - .shortName(R.string.ns_client_v3_short_name) - .description(R.string.description_ns_client_v3) + .pluginName(TextRef.AndroidRes(R.string.ns_client_v3_title)) + .shortName(TextRef.AndroidRes(R.string.ns_client_v3_short_name)) + .description(TextRef.AndroidRes(R.string.description_ns_client_v3)) .composeContent { plugin -> NSClientComposeContent( dateUtil = dateUtil, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt index a0d1e0435a76..f270e383b7a1 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt @@ -79,9 +79,9 @@ class OpenHumansUploaderPlugin @Inject internal constructor( PluginDescription() .mainType(PluginType.SYNC) .icon(IcPluginOpenHumans) - .pluginName(R.string.open_humans) - .shortName(R.string.open_humans_short) - .description(R.string.open_humans_description) + .pluginName(TextRef.AndroidRes(R.string.open_humans)) + .shortName(TextRef.AndroidRes(R.string.open_humans_short)) + .description(TextRef.AndroidRes(R.string.open_humans_description)) .composeContent { plugin -> OHComposeContent( plugin = plugin as OpenHumansUploaderPlugin, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index 0a1cdf00b8bd..d677e79a13fc 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -144,9 +144,9 @@ class SmsCommunicatorPlugin @Inject constructor( .mainType(PluginType.SYNC) .composeContent { SmsCommunicatorComposeContent() } .icon(IcPluginSms) - .pluginName(R.string.smscommunicator) - .shortName(R.string.smscommunicator_shortname) - .description(R.string.description_sms_communicator), + .pluginName(TextRef.AndroidRes(R.string.smscommunicator)) + .shortName(TextRef.AndroidRes(R.string.smscommunicator_shortname)) + .description(TextRef.AndroidRes(R.string.description_sms_communicator)), ownPreferences = SmsIntentKey.entries, aapsLogger, rh, preferences ), SmsCommunicator { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt index 5cc8ad609608..e68ba68e1f5f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/TidepoolPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.tidepool +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.time.T @@ -60,8 +61,8 @@ class TidepoolPlugin @Inject constructor( ) : Sync, Tidepool, PluginBaseWithPreferences( PluginDescription() .mainType(PluginType.SYNC) - .pluginName(R.string.tidepool) - .shortName(R.string.tidepool_shortname) + .pluginName(TextRef.AndroidRes(R.string.tidepool)) + .shortName(TextRef.AndroidRes(R.string.tidepool_shortname)) .icon(IcPluginTidepool) .composeContent { TidepoolComposeContent( @@ -77,7 +78,7 @@ class TidepoolPlugin @Inject constructor( onClearLog = { tidepoolRepository.clearLog() } ) } - .description(R.string.description_tidepool), + .description(TextRef.AndroidRes(R.string.description_tidepool)), ownPreferences = TidepoolBooleanKey.entries + TidepoolLongNonKey.entries + TidepoolStringNonKey.entries, aapsLogger, rh, preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 4880ee7d05ca..fb5b9ba194a5 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.content.pm.ResolveInfo import android.os.Bundle +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.configuration.Config @@ -69,9 +70,9 @@ class TizenPlugin @Inject constructor( PluginDescription() .mainType(PluginType.SYNC) .icon(IcPluginTizen) - .pluginName(R.string.tizen) - .shortName(R.string.tizen_short) - .description(R.string.tizen_description), + .pluginName(TextRef.AndroidRes(R.string.tizen)) + .shortName(TextRef.AndroidRes(R.string.tizen_short)) + .description(TextRef.AndroidRes(R.string.tizen_description)), aapsLogger, rh ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt index df7aa697dd48..49f84dddd5da 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Watch +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.TT import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.configuration.Config @@ -76,9 +77,9 @@ class WearPlugin @Inject constructor( pluginDescription = PluginDescription() .mainType(PluginType.SYNC) .icon(Icons.Default.Watch) - .pluginName(app.aaps.core.ui.R.string.wear) - .shortName(R.string.wear_shortname) - .description(R.string.description_wear) + .pluginName(TextRef.AndroidRes(app.aaps.core.ui.R.string.wear)) + .shortName(TextRef.AndroidRes(R.string.wear_shortname)) + .description(TextRef.AndroidRes(R.string.description_wear)) .composeContent { WearComposeContent() }, aapsLogger = aapsLogger, rh = rh, preferences = preferences ) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 3af201384c44..5ac2a9e39f2f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -9,6 +9,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.HR import app.aaps.core.data.model.SC @@ -106,9 +107,9 @@ class XdripPlugin @Inject constructor( ) } .icon(IcXDrip) - .pluginName(R.string.xdrip) - .shortName(R.string.xdrip_shortname) - .description(R.string.description_xdrip), + .pluginName(TextRef.AndroidRes(R.string.xdrip)) + .shortName(TextRef.AndroidRes(R.string.xdrip_shortname)) + .description(TextRef.AndroidRes(R.string.description_xdrip)), ownPreferences = XdripLongKey.entries + XdripIntentKey.entries, aapsLogger, rh, preferences ) { diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index fb957e8db527..f4f79ef3cae7 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -139,9 +139,9 @@ class ComboV2Plugin @Inject constructor( ) } .icon(IcPluginCombo) - .pluginName(R.string.combov2_plugin_name) - .shortName(R.string.combov2_plugin_shortname) - .description(R.string.combov2_plugin_description), + .pluginName(TextRef.AndroidRes(R.string.combov2_plugin_name)) + .shortName(TextRef.AndroidRes(R.string.combov2_plugin_shortname)) + .description(TextRef.AndroidRes(R.string.combov2_plugin_description)), ownPreferences = ComboIntKey.entries + ComboBooleanKey.entries + ComboStringNonKey.entries + ComboIntNonKey.entries + ComboLongNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, PluginConstraints { diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt index 3e3200994dbb..becdda8e2749 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.pump.danar +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription @@ -84,9 +85,9 @@ abstract class AbstractDanaRPlugin protected constructor( ) } .icon(IcPluginDanaI) - .pluginName(app.aaps.pump.dana.R.string.danarpump) - .shortName(app.aaps.pump.dana.R.string.danarpump_shortname) - .description(app.aaps.pump.dana.R.string.description_pump_dana_r), + .pluginName(TextRef.AndroidRes(app.aaps.pump.dana.R.string.danarpump)) + .shortName(TextRef.AndroidRes(app.aaps.pump.dana.R.string.danarpump_shortname)) + .description(TextRef.AndroidRes(app.aaps.pump.dana.R.string.description_pump_dana_r)), ownPreferences = DanaStringNonKey.entries + DanaIntKey.entries + DanaIntNonKey.entries + DanaBooleanKey.entries + DanaIntentKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, Dana, PumpPluginConstraints, OwnDatabasePlugin { diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt index 116e763cbacf..6bf920f8a0d5 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.logging.AAPSLogger @@ -86,7 +87,7 @@ class DanaRKoreanPlugin @Inject constructor( private var scope: CoroutineScope? = null init { - pluginDescription.description(app.aaps.pump.dana.R.string.description_pump_dana_r_korean) + pluginDescription.description(TextRef.AndroidRes(app.aaps.pump.dana.R.string.description_pump_dana_r_korean)) pumpDescription.fillFor(PumpType.DANA_R_KOREAN) } diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt index b5b164ca80d2..99ce375349a7 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.pump.defs.PumpType import app.aaps.core.data.time.T.Companion.mins import app.aaps.core.interfaces.configuration.Config @@ -105,7 +106,7 @@ class DanaRv2Plugin @Inject constructor( private var scope: CoroutineScope? = null init { - pluginDescription.description(R.string.description_pump_dana_r_v2) + pluginDescription.description(TextRef.AndroidRes(R.string.description_pump_dana_r_v2)) pumpDescription.fillFor(PumpType.DANA_RV2) } diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt index b7d1bf2bd912..590e6aaae0ca 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription @@ -95,9 +96,9 @@ class DanaRSPlugin @Inject constructor( ) } .icon(IcPluginDanaI) - .pluginName(app.aaps.pump.dana.R.string.danarspump) - .shortName(app.aaps.pump.dana.R.string.danarspump_shortname) - .description(app.aaps.pump.dana.R.string.description_pump_dana_rs), + .pluginName(TextRef.AndroidRes(app.aaps.pump.dana.R.string.danarspump)) + .shortName(TextRef.AndroidRes(app.aaps.pump.dana.R.string.danarspump_shortname)) + .description(TextRef.AndroidRes(app.aaps.pump.dana.R.string.description_pump_dana_rs)), ownPreferences = DanaStringNonKey.entries + DanaIntKey.entries + DanaBooleanKey.entries + DanaIntentKey.entries + DanaStringComposedKey.entries + DanaLongKey.entries, aapsLogger, rh, preferences, commandQueue diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt index b0f61f48867b..dba03c64bd7a 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription @@ -94,9 +95,9 @@ class DiaconnG8Plugin @Inject constructor( ) } .icon(IcPluginDiaconn) - .pluginName(R.string.diaconn_g8_pump) - .shortName(R.string.diaconn_g8_pump_shortname) - .description(R.string.description_pump_diaconn_g8), + .pluginName(TextRef.AndroidRes(R.string.diaconn_g8_pump)) + .shortName(TextRef.AndroidRes(R.string.diaconn_g8_pump_shortname)) + .description(TextRef.AndroidRes(R.string.description_pump_diaconn_g8)), ownPreferences = DiaconnIntentKey.entries + DiaconnIntKey.entries + DiaconnBooleanKey.entries + DiaconnStringNonKey.entries + DiaconnIntNonKey.entries, aapsLogger, rh, preferences, commandQueue diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index 3dc21da22c66..0bc51336dcd9 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -104,9 +104,9 @@ class EopatchPumpPlugin @Inject constructor( ) } .icon(IcPluginEopatch) - .pluginName(R.string.eopatch) - .shortName(R.string.eopatch_shortname) - .description(R.string.eopatch_pump_description), + .pluginName(TextRef.AndroidRes(R.string.eopatch)) + .shortName(TextRef.AndroidRes(R.string.eopatch_shortname)) + .description(TextRef.AndroidRes(R.string.eopatch_pump_description)), ownPreferences = EopatchIntKey.entries + EopatchBooleanKey.entries + EopatchStringNonKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump { diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index 5af576bffc9a..67cdc1488cd6 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -2,6 +2,7 @@ package app.aaps.pump.equil import android.content.Context import android.os.SystemClock +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.plugin.PluginType import app.aaps.core.data.pump.defs.ManufacturerType import app.aaps.core.data.pump.defs.PumpDescription @@ -105,9 +106,9 @@ class EquilPumpPlugin @Inject constructor( ) } .icon(IcPluginEquil) - .pluginName(R.string.equil_name) - .shortName(R.string.equil_name_short) - .description(R.string.equil_pump_description), + .pluginName(TextRef.AndroidRes(R.string.equil_name)) + .shortName(TextRef.AndroidRes(R.string.equil_name_short)) + .description(TextRef.AndroidRes(R.string.equil_pump_description)), ownPreferences = EquilBooleanKey.entries + EquilBooleanPreferenceKey.entries + EquilIntPreferenceKey.entries + EquilStringKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump { diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt index 5159595a6081..c586654ef40d 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt @@ -159,10 +159,10 @@ class InsightPlugin @Inject constructor( ) : PumpPluginBase( pluginDescription = PluginDescription() .icon(IcPluginInsight) - .pluginName(R.string.insight_local) - .shortName(R.string.insightpump_shortname) + .pluginName(TextRef.AndroidRes(R.string.insight_local)) + .shortName(TextRef.AndroidRes(R.string.insightpump_shortname)) .mainType(PluginType.PUMP) - .description(R.string.description_pump_insight_local) + .description(TextRef.AndroidRes(R.string.description_pump_insight_local)) .composeContent { plugin -> InsightComposeContent( insightPlugin = plugin as InsightPlugin, diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index f6eca750c452..1befed360c28 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -150,9 +150,9 @@ class MedtronicPumpPlugin @Inject constructor( ) } .icon(IcPluginMedtronic) - .pluginName(R.string.medtronic_name) - .shortName(R.string.medtronic_name_short) - .description(R.string.description_pump_medtronic), + .pluginName(TextRef.AndroidRes(R.string.medtronic_name)) + .shortName(TextRef.AndroidRes(R.string.medtronic_name_short)) + .description(TextRef.AndroidRes(R.string.description_pump_medtronic)), ownPreferences = RileylinkBooleanPreferenceKey.entries + RileyLinkDoubleKey.entries + RileyLinkLongKey.entries + RileyLinkStringKey.entries + RileyLinkStringPreferenceKey.entries + MedtronicBooleanPreferenceKey.entries + MedtronicIntPreferenceKey.entries + MedtronicLongNonKey.entries + MedtronicStringPreferenceKey.entries, diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index 768d0f9b295b..63f9f6c8d992 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -83,9 +83,9 @@ class MedtrumPlugin @Inject constructor( pluginDescription = PluginDescription() .mainType(PluginType.PUMP) .icon(IcPluginMedtrum) - .pluginName(R.string.medtrum) - .shortName(R.string.medtrum_pump_shortname) - .description(R.string.medtrum_pump_description) + .pluginName(TextRef.AndroidRes(R.string.medtrum)) + .shortName(TextRef.AndroidRes(R.string.medtrum_pump_shortname)) + .description(TextRef.AndroidRes(R.string.medtrum_pump_description)) .composeContent { _ -> MedtrumComposeContent( pluginName = rh.gs(R.string.medtrum), diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 7164ae8f9540..36e96c2782ee 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -133,9 +133,9 @@ class OmnipodDashPumpPlugin @Inject constructor( ) } .icon(IcPluginOmnipod) - .pluginName(R.string.omnipod_dash_name) - .shortName(R.string.omnipod_dash_name_short) - .description(R.string.omnipod_dash_pump_description), + .pluginName(TextRef.AndroidRes(R.string.omnipod_dash_name)) + .shortName(TextRef.AndroidRes(R.string.omnipod_dash_name_short)) + .description(TextRef.AndroidRes(R.string.omnipod_dash_pump_description)), ownPreferences = OmnipodBooleanPreferenceKey.entries + OmnipodIntPreferenceKey.entries + DashBooleanPreferenceKey.entries + DashStringNonPreferenceKey.entries, aapsLogger, rh, preferences, commandQueue diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index 3b98f0e25e66..011e0aec7bd4 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -158,9 +158,9 @@ class OmnipodErosPumpPlugin @Inject constructor( ) } .icon(IcPluginOmnipod) - .pluginName(R.string.omnipod_eros_name) - .shortName(R.string.omnipod_eros_name_short) - .description(R.string.omnipod_eros_pump_description), + .pluginName(TextRef.AndroidRes(R.string.omnipod_eros_name)) + .shortName(TextRef.AndroidRes(R.string.omnipod_eros_name_short)) + .description(TextRef.AndroidRes(R.string.omnipod_eros_pump_description)), ownPreferences = ErosBooleanPreferenceKey.entries + ErosLongNonPreferenceKey.entries + ErosStringNonPreferenceKey.entries, aapsLogger, rh, preferences, commandQueue ), Pump, RileyLinkPumpDevice, OmnipodEros, OwnDatabasePlugin { diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt index ba624870e958..1d889547ec3e 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt @@ -91,9 +91,9 @@ open class VirtualPumpPlugin @Inject constructor( ) } .icon(IcPluginVirtualPump) - .pluginName(app.aaps.core.ui.R.string.virtual_pump) - .shortName(R.string.virtual_pump_shortname) - .description(R.string.description_pump_virtual) + .pluginName(TextRef.AndroidRes(app.aaps.core.ui.R.string.virtual_pump)) + .shortName(TextRef.AndroidRes(R.string.virtual_pump_shortname)) + .description(TextRef.AndroidRes(R.string.description_pump_virtual)) .setDefault() .showInList { !config.AAPSCLIENT }, ownPreferences = VirtualBooleanNonPreferenceKey.entries, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainNavigationBar.kt b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainNavigationBar.kt index 038261e4e87d..6d531ff37a00 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainNavigationBar.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainNavigationBar.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.ui.compose.AapsTheme @@ -142,7 +143,7 @@ fun MainNavigationBar( // Pump setup (visible only when pump not initialized and has compose content) if (pumpSetupPlugin != null) { - val label = stringResource(pumpSetupPlugin.pluginDescription.pluginName) + val label = pumpSetupPlugin.pluginDescription.pluginName?.let { stringResource(it) } ?: pumpSetupPlugin.name NavigationBarItem( selected = false, onClick = { onNavigate(NavigationRequest.Plugin(pumpSetupPlugin.javaClass.simpleName)) }, @@ -165,7 +166,7 @@ fun MainNavigationBar( // BG source shortcut (visible when BG quality check reports FLAT or DOUBLED) val bgIcon = bgSetupPlugin?.pluginDescription?.icon if (bgSetupPlugin != null && bgIcon != null) { - val label = stringResource(bgSetupPlugin.pluginDescription.pluginName) + val label = bgSetupPlugin.pluginDescription.pluginName?.let { stringResource(it) } ?: bgSetupPlugin.name NavigationBarItem( selected = false, onClick = { onNavigate(NavigationRequest.Plugin(bgSetupPlugin.javaClass.simpleName)) }, @@ -201,7 +202,7 @@ fun MainNavigationBar( // Objectives progress (visible while any objective is not yet accomplished) val objectivesIcon = objectivesSetupPlugin?.pluginDescription?.icon if (objectivesSetupPlugin != null && objectivesIcon != null) { - val label = stringResource(objectivesSetupPlugin.pluginDescription.pluginName) + val label = objectivesSetupPlugin.pluginDescription.pluginName?.let { stringResource(it) } ?: objectivesSetupPlugin.name NavigationBarItem( selected = false, onClick = { onNavigate(NavigationRequest.Plugin(objectivesSetupPlugin.javaClass.simpleName)) }, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/preferences/PreferenceScreenView.kt b/ui/src/main/kotlin/app/aaps/ui/compose/preferences/PreferenceScreenView.kt index 674e497e8f88..f79b36133167 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/preferences/PreferenceScreenView.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/preferences/PreferenceScreenView.kt @@ -22,6 +22,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import app.aaps.core.ui.compose.stringResource +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.compose.AapsTopAppBar import app.aaps.core.ui.compose.ComposeScreenContent import app.aaps.core.ui.compose.LocalSnackbarHostState @@ -51,11 +53,9 @@ fun PreferenceScreenView( highlightKey: String? = null, onBackClick: () -> Unit ) { - val title = if (screenDef.titleResId != 0) { - stringResource(screenDef.titleResId) - } else { - screenDef.key - } + // A screen built with titleResId = 0 has no title of its own and falls back to its key. + val titleRef = screenDef.title + val title = if (titleRef is TextRef.AndroidRes && titleRef.id == 0) screenDef.key else stringResource(titleRef) val sectionState = rememberPreferenceSectionState() val snackbarHostState = LocalSnackbarHostState.current diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt index fd24ee887806..e24a3830efe2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/quickLaunch/QuickLaunchResolver.kt @@ -131,7 +131,7 @@ class QuickLaunchResolver @Inject constructor( is QuickLaunchAction.ProfileAction -> buildProfileLabel(action) is QuickLaunchAction.SceneAction -> sceneRepository.getScene(action.sceneId)?.name ?: "?" - is QuickLaunchAction.PluginAction -> findPlugin(action.className)?.let { rh.gs(it.pluginDescription.pluginName) } ?: "?" + is QuickLaunchAction.PluginAction -> findPlugin(action.className)?.name ?: "?" else -> { val label = action.elementType?.label() @@ -178,7 +178,7 @@ class QuickLaunchResolver @Inject constructor( } is QuickLaunchAction.PluginAction -> findPlugin(action.className) - ?.pluginDescription?.description?.takeIf { it != -1 }?.let { rh.gs(it) } + ?.pluginDescription?.description?.let { rh.gs(it) } else -> { val desc = action.elementType?.description() @@ -189,8 +189,8 @@ class QuickLaunchResolver @Inject constructor( fun resolvePluginItem(plugin: PluginBase): ResolvedQuickLaunchItem { val action = QuickLaunchAction.PluginAction(plugin.javaClass.simpleName) val icon = plugin.pluginDescription.icon ?: Icons.Default.Extension - val label = rh.gs(plugin.pluginDescription.pluginName) - val desc = plugin.pluginDescription.description.takeIf { it != -1 }?.let { rh.gs(it) } + val label = plugin.name + val desc = plugin.pluginDescription.description?.let { rh.gs(it) } return ResolvedQuickLaunchItem( action = action, label = label, diff --git a/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt b/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt index a11028467bcb..06f4f86b51e0 100644 --- a/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt +++ b/ui/src/main/kotlin/app/aaps/ui/search/SearchIndexBuilder.kt @@ -167,7 +167,7 @@ class SearchIndexBuilder @Inject constructor( * row drops out of search. */ private fun PluginBase.isListVisible(): Boolean = - categoryAvailable() && showInList(pluginDescription.mainType) && pluginDescription.pluginName != -1 + categoryAvailable() && showInList(pluginDescription.mainType) && pluginDescription.pluginName != null /** * A plugin's settings (screen/category and individual preference keys) are searchable when the @@ -177,7 +177,7 @@ class SearchIndexBuilder @Inject constructor( * so its settings stay out of search — the values come from the master and aren't editable locally. */ private fun PluginBase.hasSearchableSettings(): Boolean = - categoryAvailable() && (showInList(pluginDescription.mainType) || pluginDescription.alwaysEnabled) && pluginDescription.pluginName != -1 + categoryAvailable() && (showInList(pluginDescription.mainType) || pluginDescription.alwaysEnabled) && pluginDescription.pluginName != null private fun collectPlugins(entries: MutableList, seenKeys: MutableSet) { activePlugin.getPluginsList() From 9d1eb070bb4a90a86e3ecb864f3ae56230f85e9a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 07:36:36 +0200 Subject: [PATCH 115/146] ActivePlugin, PluginBase and the command queue reach commonMain PluginBase held rh: ResourceHelper, but it only ever calls gs(TextRef), for name, nameShort and description. ResourceHelper adds the Android resource id overloads, which mean something only where Android resources exist, so that one field kept PluginBase, PluginDescription and ActivePlugin in androidMain. rh is now TextResolver, the platform neutral half. Plugins that use the inherited rh with Android resource ids narrow it back with "override val rh: ResourceHelper" - 31 one line changes. The alternative was to wrap every call as rh.gs(TextRef.AndroidRes(R.string.x)), which measured 366 call sites and would also mix the TextRef migration of individual plugins into a change that is about platform neutrality. That migration stays a separate job. Plugins behave exactly as before. Other platform specific parts of these types: - ActivePlugin.getSpecificPluginsListByInterface and CommandQueue.isCustomCommand* took Class<*>. They take KClass now, so call sites pass X::class. - ActivePlugin.collectMissingPermissions and collectAllPermissions need a Context and the Android permission model. They moved to a new PluginPermissions interface in androidMain; PluginStore implements both. - UiInteraction.mainActivity and errorHelperActivity are KClass, so the nine Intent call sites add .java. - Callback implemented java.lang.Runnable. Nothing ever handed a Callback to something that wanted a Runnable, so it declares its own run(). This also fixed two "cannot infer type parameter" errors in Command, where ?.run() resolved to Kotlin's scope function instead. Moved to commonMain: ActivePlugin, PluginBase, PluginBaseWithPreferences, PluginDescription, APS, Sensitivity, IobCobCalculator, PumpWithConcentration, Callback, Command, CommandQueue, EffectiveProfile, UiInteraction. :core:interfaces goes from 211/49 to 224/37 commonMain/androidMain. Dead code found on the way: - CommandQueueImplementation.removeAllCustomCommands tested "command is CustomCommand" on a Command from the queue, so it never matched. It was unreachable anyway: the line above returns early whenever a command of the same type is already queued. - OmnipodDashPumpPlugin built a bolus progress string and threw it away. Omnipod Eros keeps rh.gq for its pod alert plural. Plurals have no TextRef form yet, so that plugin still needs the Android ResourceHelper. --- .../aps/openAPSAMA/TestOpenAPSAMAPlugin.kt | 2 +- .../aps/openAPSSMB/TestOpenAPSSMBPlugin.kt | 2 +- .../kotlin/app/aaps/ComposeMainActivity.kt | 3 ++ .../aaps/compose/navigation/AppNavGraph.kt | 6 ++-- .../aaps/implementations/UiInteractionImpl.kt | 7 ++-- .../interfaces/plugin/PluginPermissions.kt | 24 ++++++++++++++ .../core/interfaces/pump/PumpPluginBase.kt | 2 +- .../aaps/core/interfaces/queue/Callback.kt | 12 ------- .../app/aaps/core/interfaces/aps/APS.kt | 0 .../aaps/core/interfaces/aps/Sensitivity.kt | 0 .../core/interfaces/iob/IobCobCalculator.kt | 0 .../core/interfaces/plugin/ActivePlugin.kt | 16 ++-------- .../aaps/core/interfaces/plugin/PluginBase.kt | 4 +-- .../plugin/PluginBaseWithPreferences.kt | 4 +-- .../interfaces/plugin/PluginDescription.kt | 0 .../interfaces/profile/EffectiveProfile.kt | 0 .../interfaces/pump/PumpWithConcentration.kt | 0 .../aaps/core/interfaces/queue/Callback.kt | 21 ++++++++++++ .../app/aaps/core/interfaces/queue/Command.kt | 0 .../core/interfaces/queue/CommandQueue.kt | 5 +-- .../aaps/core/interfaces/ui/UiInteraction.kt | 7 ++-- .../AlarmNotificationManager.kt | 8 ++--- .../NotificationHolderImpl.kt | 4 +-- .../implementation/di/ImplementationModule.kt | 2 ++ .../aaps/implementation/plugin/PluginStore.kt | 18 ++++++----- .../queue/CommandQueueImplementation.kt | 20 +++--------- .../queue/CommandQueueImplementationTest.kt | 12 +++---- .../plugins/aps/autotune/AutotunePlugin.kt | 2 +- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 6 ++-- .../aps/openAPSAMA/OpenAPSAMAPlugin.kt | 2 +- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 2 +- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 2 +- .../plugins/automation/AutomationRuntime.kt | 2 +- .../automation/actions/ActionRunAutotune.kt | 2 +- .../calibration/LinearCalibrationPlugin.kt | 2 +- .../configBuilder/ConfigBuilderImpl.kt | 14 ++++---- .../constraints/ConstraintsCheckerImpl.kt | 32 +++++++++---------- .../bgQualityCheck/BgQualityCheckPlugin.kt | 2 +- .../objectives/ObjectivesPlugin.kt | 2 +- .../objectives/objectives/Objective0.kt | 2 +- .../constraints/safety/SafetyPlugin.kt | 2 +- .../storage/StorageConstraintPlugin.kt | 2 +- .../versionChecker/VersionCheckerPlugin.kt | 2 +- .../constraints/ConstraintsCheckerImplTest.kt | 2 +- .../PersistentNotificationPlugin.kt | 7 ++-- .../sensitivity/SensitivityOref1Plugin.kt | 2 +- .../plugins/source/NSClientSourcePlugin.kt | 2 +- .../sync/nsclientV3/NSClientV3Plugin.kt | 2 +- .../openhumans/OpenHumansUploaderPlugin.kt | 2 +- .../smsCommunicator/SmsCommunicatorPlugin.kt | 2 +- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 2 +- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 2 +- .../nightscout/pump/combov2/ComboV2Plugin.kt | 6 ++-- .../aaps/pump/danar/AbstractDanaRPlugin.kt | 2 +- .../kotlin/app/aaps/pump/danar/DanaRPlugin.kt | 2 +- .../pump/danarkorean/DanaRKoreanPlugin.kt | 2 +- .../app/aaps/pump/danarv2/DanaRv2Plugin.kt | 2 +- .../app/aaps/pump/danars/DanaRSPlugin.kt | 2 +- .../app/aaps/pump/diaconn/DiaconnG8Plugin.kt | 2 +- .../aaps/pump/eopatch/EopatchPumpPlugin.kt | 2 +- .../app/aaps/pump/equil/EquilPumpPlugin.kt | 2 +- .../app/aaps/pump/insight/InsightPlugin.kt | 2 +- .../pump/medtronic/MedtronicPumpPlugin.kt | 2 +- .../app/aaps/pump/medtrum/MedtrumPlugin.kt | 2 +- .../omnipod/dash/OmnipodDashPumpPlugin.kt | 11 +++---- .../dash/ui/compose/DashOverviewViewModel.kt | 6 ++-- .../omnipod/eros/OmnipodErosPumpPlugin.kt | 6 ++-- .../eros/ui/compose/ErosOverviewViewModel.kt | 10 +++--- .../aaps/pump/virtual/VirtualPumpPlugin.kt | 2 +- .../maintenance/MaintenanceViewModel.kt | 2 +- .../permissionsSheet/PermissionsViewModel.kt | 8 ++--- .../preferences/AllPreferencesScreen.kt | 2 +- .../PermissionsViewModelTest.kt | 10 +++--- 73 files changed, 190 insertions(+), 175 deletions(-) create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginPermissions.kt delete mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/APS.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt (87%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt (98%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt (95%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt (100%) create mode 100644 core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/queue/Command.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt (93%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt (84%) diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt index a1e84e1452ce..f3a27792416c 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt @@ -57,7 +57,7 @@ class TestOpenAPSAMAPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, private val constraintChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, private val activePlugin: ActivePlugin, private val iobCobCalculator: IobCobCalculator, diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt index 4a88ba986ebd..5cb35da13080 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt @@ -54,7 +54,7 @@ open class TestOpenAPSSMBPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, private val constraintChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, val context: Context, private val activePlugin: ActivePlugin, diff --git a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt index fca72aab6c9b..28d30d992418 100644 --- a/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt +++ b/app/src/main/kotlin/app/aaps/ComposeMainActivity.kt @@ -87,6 +87,7 @@ import app.aaps.core.interfaces.notifications.NotificationLevel import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.overview.graph.OverviewDataCache import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.plugin.PluginPermissions import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.protection.ExportPasswordDataStore @@ -193,6 +194,7 @@ class ComposeMainActivity : AppCompatActivity() { @Inject lateinit var cryptoUtil: CryptoUtil @Inject lateinit var exportPasswordDataStore: ExportPasswordDataStore @Inject lateinit var activePlugin: ActivePlugin + @Inject lateinit var pluginPermissions: PluginPermissions @Inject lateinit var nsClient: NsClient @Inject lateinit var clientControlActionDispatcher: ClientControlActionDispatcher @Inject lateinit var automationRuntime: AutomationRuntime @@ -790,6 +792,7 @@ class ComposeMainActivity : AppCompatActivity() { swDefinition = swDefinition, rxBus = rxBus, activePlugin = activePlugin, + pluginPermissions = pluginPermissions, automationRuntime = automationRuntime, preferences = preferences, rh = rh, diff --git a/app/src/main/kotlin/app/aaps/compose/navigation/AppNavGraph.kt b/app/src/main/kotlin/app/aaps/compose/navigation/AppNavGraph.kt index 6a06de50a95e..22c60d643384 100644 --- a/app/src/main/kotlin/app/aaps/compose/navigation/AppNavGraph.kt +++ b/app/src/main/kotlin/app/aaps/compose/navigation/AppNavGraph.kt @@ -40,6 +40,7 @@ import app.aaps.core.interfaces.maintenance.FileListProvider import app.aaps.core.interfaces.navigation.ElementType import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PermissionGroup +import app.aaps.core.interfaces.plugin.PluginPermissions import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.protection.ProtectionCheck import app.aaps.core.interfaces.resources.ResourceHelper @@ -148,6 +149,7 @@ fun NavGraphBuilder.appNavGraph( swDefinition: SWDefinition, rxBus: RxBus, activePlugin: ActivePlugin, + pluginPermissions: PluginPermissions, automationRuntime: AutomationRuntime, preferences: Preferences, rh: ResourceHelper, @@ -749,8 +751,8 @@ fun NavGraphBuilder.appNavGraph( onRequestDirectoryAccess = onRequestDirectoryAccess, onRequestPermission = onRequestPermission, permissionItems = { - val allGroups = activePlugin.collectAllPermissions(navController.context) - val missingGroups = activePlugin.collectMissingPermissions(navController.context) + val allGroups = pluginPermissions.collectAllPermissions(navController.context) + val missingGroups = pluginPermissions.collectMissingPermissions(navController.context) val missingSets = missingGroups.map { it.permissions.toSet() }.toSet() allGroups.map { group -> group to (group.permissions.toSet() !in missingSets) } }, diff --git a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt index c37d3d6dd493..ca7fc49cb2e5 100644 --- a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt +++ b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Provider +import kotlin.reflect.KClass @Reusable class UiInteractionImpl @Inject constructor( @@ -42,8 +43,8 @@ class UiInteractionImpl @Inject constructor( @ApplicationScope private val appScope: CoroutineScope ) : UiInteraction { - override val mainActivity: Class<*> = ComposeMainActivity::class.java - override val errorHelperActivity: Class<*> = ErrorActivity::class.java + override val mainActivity: KClass<*> = ComposeMainActivity::class + override val errorHelperActivity: KClass<*> = ErrorActivity::class override fun runAlarm(status: String, title: String, sound: AlarmSound?) { // Persist the error as an announcement at fire time — gated by the NS-announcement @@ -78,7 +79,7 @@ class UiInteractionImpl @Inject constructor( // • Works because the caller's process is already foreground (Android's // background-activity-start restriction does not apply). aapsLogger.debug(LTag.CORE, "runAlarm (foreground direct): $title - $status (sound=$sound)") - val intent = Intent(context, errorHelperActivity).apply { + val intent = Intent(context, errorHelperActivity.java).apply { putExtra(AlarmIntent.EXTRA_SOUND, sound?.name) putExtra(AlarmIntent.EXTRA_STATUS, status) putExtra(AlarmIntent.EXTRA_TITLE, title) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginPermissions.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginPermissions.kt new file mode 100644 index 000000000000..95de55f0e0d9 --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginPermissions.kt @@ -0,0 +1,24 @@ +package app.aaps.core.interfaces.plugin + +import android.content.Context + +/** + * Permission state collected across all enabled plugins. + * + * Kept apart from [ActivePlugin] because it needs a [Context] and the Android permission model, + * and both exist only on Android. The same store implements both interfaces. + */ +interface PluginPermissions { + + /** + * Collects missing permissions across all enabled plugins, deduplicated by permission set. + */ + fun collectMissingPermissions(context: Context): List + + /** + * Collects all required permissions (both global and plugin-declared), + * regardless of grant status. Used by the permission UI to show both + * granted and missing permissions. + */ + fun collectAllPermissions(context: Context): List +} diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt index b9bfc7ce44ba..4f7cb7a8bf4a 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpPluginBase.kt @@ -26,7 +26,7 @@ abstract class PumpPluginBase( pluginDescription: PluginDescription, ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, val commandQueue: CommandQueue ) : PluginBaseWithPreferences(pluginDescription, ownPreferences, aapsLogger, rh, preferences) { diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt deleted file mode 100644 index 19eda218aa09..000000000000 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.aaps.core.interfaces.queue - -import app.aaps.core.interfaces.pump.PumpEnactResult - -abstract class Callback : Runnable { - - lateinit var result: PumpEnactResult - fun result(result: PumpEnactResult): Callback { - this.result = result - return this - } -} \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APS.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/APS.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/APS.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/APS.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Sensitivity.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/iob/IobCobCalculator.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt similarity index 87% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt index ce0ba77369a6..74b079f3d990 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/ActivePlugin.kt @@ -1,6 +1,5 @@ package app.aaps.core.interfaces.plugin -import android.content.Context import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.aps.APS import app.aaps.core.interfaces.aps.Sensitivity @@ -13,6 +12,7 @@ import app.aaps.core.interfaces.pump.PumpWithConcentration import app.aaps.core.interfaces.smoothing.Smoothing import app.aaps.core.interfaces.source.BgSource import app.aaps.core.interfaces.sync.Sync +import kotlin.reflect.KClass interface ActivePlugin { @@ -94,7 +94,7 @@ interface ActivePlugin { /** * List of all plugins implementing interface */ - fun getSpecificPluginsListByInterface(interfaceClass: Class<*>): ArrayList + fun getSpecificPluginsListByInterface(interfaceClass: KClass<*>): ArrayList /** * Pre-process all plugin types and validate active plugins (ie. only only one plugin for type is selected) @@ -134,16 +134,4 @@ interface ActivePlugin { * See: [app.aaps.core.interfaces.maintenance.ImportExportPrefs.doImportSharedPreferences] */ fun afterImport() - - /** - * Collects missing permissions across all enabled plugins, deduplicated by permission set. - */ - fun collectMissingPermissions(context: Context): List - - /** - * Collects all required permissions (both global and plugin-declared), - * regardless of grant status. Used by the permission UI to show both - * granted and missing permissions. - */ - fun collectAllPermissions(context: Context): List } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt similarity index 98% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt index 3c97816a010c..5c55dd4a835f 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBase.kt @@ -3,7 +3,7 @@ package app.aaps.core.interfaces.plugin import app.aaps.core.data.plugin.PluginType import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.keys.interfaces.PreferenceItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +17,7 @@ import kotlinx.coroutines.runBlocking abstract class PluginBase( val pluginDescription: PluginDescription, val aapsLogger: AAPSLogger, - val rh: ResourceHelper + open val rh: TextResolver ) { protected val pluginScope = CoroutineScope(Dispatchers.Default + Job()) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt similarity index 95% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt index 90f825d8397f..8c6028d30a91 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginBaseWithPreferences.kt @@ -1,7 +1,7 @@ package app.aaps.core.interfaces.plugin import app.aaps.core.interfaces.logging.AAPSLogger -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.keys.interfaces.NonPreferenceKey import app.aaps.core.keys.interfaces.Preferences @@ -12,7 +12,7 @@ abstract class PluginBaseWithPreferences( pluginDescription: PluginDescription, val ownPreferences: List = emptyList(), aapsLogger: AAPSLogger, - rh: ResourceHelper, + rh: TextResolver, val preferences: Preferences ) : PluginBase(pluginDescription, aapsLogger, rh) { diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/plugin/PluginDescription.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/EffectiveProfile.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/PumpWithConcentration.kt diff --git a/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt new file mode 100644 index 000000000000..72464b80eff8 --- /dev/null +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Callback.kt @@ -0,0 +1,21 @@ +package app.aaps.core.interfaces.queue + +import app.aaps.core.interfaces.pump.PumpEnactResult + +/** + * Result handler for a queued [Command]. + * + * Declares its own [run] instead of implementing `java.lang.Runnable`. Nothing ever handed a + * Callback to something that wanted a Runnable, and Runnable is JVM only, so the supertype only + * tied this file to one platform. + */ +abstract class Callback { + + lateinit var result: PumpEnactResult + fun result(result: PumpEnactResult): Callback { + this.result = result + return this + } + + abstract fun run() +} diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Command.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Command.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/Command.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/Command.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt similarity index 93% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt index 8146a416afd0..56f39597c6aa 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/queue/CommandQueue.kt @@ -6,6 +6,7 @@ import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.pump.PumpSync +import kotlin.reflect.KClass /** * **Deadlock warning** — the queue is processed by a single app-owned `CommandExecutor` loop; one @@ -55,8 +56,8 @@ interface CommandQueue { suspend fun deactivate(): PumpEnactResult suspend fun updateTime(): PumpEnactResult suspend fun customCommand(customCommand: CustomCommand): PumpEnactResult - fun isCustomCommandRunning(customCommandType: Class): Boolean - fun isCustomCommandInQueue(customCommandType: Class): Boolean + fun isCustomCommandRunning(customCommandType: KClass): Boolean + fun isCustomCommandInQueue(customCommandType: KClass): Boolean fun statusAsAnnotated(): AnnotatedString suspend fun isThisProfileSet(requestedProfile: EffectiveProfile): Boolean } \ No newline at end of file diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt similarity index 84% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt index adcf9a8cd653..973263bff777 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt +++ b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt @@ -1,18 +1,19 @@ package app.aaps.core.interfaces.ui import app.aaps.core.interfaces.notifications.AlarmSound +import kotlin.reflect.KClass /** * Interface to use activities located in different modules - * usage: startActivity(Intent(context, activityNames.xxxx)) + * usage: startActivity(Intent(context, activityNames.xxxx.java)) */ interface UiInteraction { /** The main activity of the application. */ - val mainActivity: Class<*> + val mainActivity: KClass<*> /** The activity for displaying error information. */ - val errorHelperActivity: Class<*> + val errorHelperActivity: KClass<*> /** * Show ErrorHelperActivity and start alarm. diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt index 225139d4c5f7..2531e89ece0b 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/AlarmNotificationManager.kt @@ -213,8 +213,8 @@ class AlarmNotificationManager @Inject constructor( private fun openAppPendingIntent(): PendingIntent? { val mainActivity = uiInteractionProvider.get().mainActivity return TaskStackBuilder.create(context).run { - addParentStack(mainActivity) - addNextIntent(Intent(context, mainActivity)) + addParentStack(mainActivity.java) + addNextIntent(Intent(context, mainActivity.java)) getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } } @@ -250,7 +250,7 @@ class AlarmNotificationManager @Inject constructor( // double-audio; if ErrorActivity does launch, it re-requests the same owner+sound, which the // player treats as idempotent (no restart glitch). val postedAt = SystemClock.elapsedRealtime() - val intent = Intent(context, uiInteractionProvider.get().errorHelperActivity).apply { + val intent = Intent(context, uiInteractionProvider.get().errorHelperActivity.java).apply { putExtra(AlarmIntent.EXTRA_SOUND, sound?.name) putExtra(AlarmIntent.EXTRA_STATUS, status) putExtra(AlarmIntent.EXTRA_TITLE, title) @@ -332,7 +332,7 @@ class AlarmNotificationManager @Inject constructor( val triggerAt = System.currentTimeMillis() + SCREEN_WAKE_DELAY_MS val show = PendingIntent.getActivity( context, WAKE_REQUEST_CODE, - Intent(context, uiInteractionProvider.get().mainActivity), + Intent(context, uiInteractionProvider.get().mainActivity.java), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val wakeOp = PendingIntent.getBroadcast( diff --git a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt index 700a140308d3..5c63d4c137c7 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/androidNotification/NotificationHolderImpl.kt @@ -34,8 +34,8 @@ class NotificationHolderImpl @Inject constructor( get() = _notification ?: placeholderNotification() override fun openAppIntent(): PendingIntent? = TaskStackBuilder.create(context).run { - addParentStack(uiInteraction.mainActivity) - addNextIntent(Intent(context, uiInteraction.mainActivity)) + addParentStack(uiInteraction.mainActivity.java) + addNextIntent(Intent(context, uiInteraction.mainActivity.java)) getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt b/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt index 57fdb926829c..9ea364779ecf 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/di/ImplementationModule.kt @@ -19,6 +19,7 @@ import app.aaps.core.interfaces.overview.LastBgData import app.aaps.core.interfaces.overview.OverviewData import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PermissionProvider +import app.aaps.core.interfaces.plugin.PluginPermissions import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.profile.ProfileRepository import app.aaps.core.interfaces.profile.ProfileStore @@ -149,6 +150,7 @@ class ImplementationModule { @Binds fun bindVisibilityContext(impl: VisibilityContextImpl): VisibilityContext @Binds fun bindFabricPrivacy(fabricPrivacyImpl: FabricPrivacyImpl): FabricPrivacy @Binds fun bindActivePlugin(pluginStore: PluginStore): ActivePlugin + @Binds fun bindPluginPermissions(pluginStore: PluginStore): PluginPermissions // Runtime-permission sources for non-plugin features (e.g. standalone Automation). // May be empty; contributors bind via @IntoSet PermissionProvider. diff --git a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt index 44d1a56aa451..ed5886cf69ed 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/plugin/PluginStore.kt @@ -24,6 +24,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PermissionGroup import app.aaps.core.interfaces.plugin.PermissionProvider import app.aaps.core.interfaces.plugin.PluginBase +import app.aaps.core.interfaces.plugin.PluginPermissions import app.aaps.core.interfaces.plugin.missingPermissions import app.aaps.core.interfaces.plugin.PluginBaseWithPreferences import app.aaps.core.interfaces.pump.Pump @@ -38,6 +39,7 @@ import app.aaps.implementation.R import dagger.Lazy import javax.inject.Inject import javax.inject.Singleton +import kotlin.reflect.KClass @Singleton class PluginStore @Inject constructor( @@ -47,7 +49,7 @@ class PluginStore @Inject constructor( // Lazy: a PermissionProvider (e.g. AutomationRuntime) transitively depends on ActivePlugin // (= this PluginStore), so eager injection would form a Dagger dependency cycle. private val permissionProviders: Lazy> -) : ActivePlugin { +) : ActivePlugin, PluginPermissions { companion object { @@ -143,10 +145,10 @@ class PluginStore @Inject constructor( } } - override fun getSpecificPluginsListByInterface(interfaceClass: Class<*>): ArrayList { + override fun getSpecificPluginsListByInterface(interfaceClass: KClass<*>): ArrayList { val newList = ArrayList() for (p in plugins) { - if (!interfaceClass.isAssignableFrom(ConfigBuilder::class.java) && interfaceClass.isAssignableFrom(p.javaClass)) newList.add(p) + if (!interfaceClass.java.isAssignableFrom(ConfigBuilder::class.java) && interfaceClass.isInstance(p)) newList.add(p) } return newList } @@ -280,20 +282,20 @@ class PluginStore @Inject constructor( get() = activeCalibrationStore ?: checkNotNull(activeCalibrationStore) { "No calibration selected" } override val activeSafety: Safety - get() = getSpecificPluginsListByInterface(Safety::class.java).first() as Safety + get() = getSpecificPluginsListByInterface(Safety::class).first() as Safety override val activeIobCobCalculator: IobCobCalculator - get() = getSpecificPluginsListByInterface(IobCobCalculator::class.java).first() as IobCobCalculator + get() = getSpecificPluginsListByInterface(IobCobCalculator::class).first() as IobCobCalculator override val activeObjectives: Objectives? - get() = getSpecificPluginsListByInterface(Objectives::class.java).firstOrNull() as Objectives? + get() = getSpecificPluginsListByInterface(Objectives::class).firstOrNull() as Objectives? @Suppress("UNCHECKED_CAST") override val firstActiveSync: Sync? - get() = (getSpecificPluginsListByInterface(Sync::class.java) as ArrayList).firstOrNull { it.connected } + get() = (getSpecificPluginsListByInterface(Sync::class) as ArrayList).firstOrNull { it.connected } @Suppress("UNCHECKED_CAST") override val activeSyncs: ArrayList - get() = getSpecificPluginsListByInterface(Sync::class.java) as ArrayList + get() = getSpecificPluginsListByInterface(Sync::class) as ArrayList override fun getPluginsList(): ArrayList = ArrayList(plugins) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index f629449568af..e14920511684 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -81,6 +81,7 @@ import java.util.LinkedList import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton +import kotlin.reflect.KClass import kotlin.time.Duration.Companion.milliseconds @OpenForTesting @@ -729,8 +730,7 @@ class CommandQueueImplementation @Inject constructor( } override suspend fun customCommand(customCommand: CustomCommand): PumpEnactResult { - if (isCustomCommandInQueue(customCommand.javaClass)) return executingNowError() - removeAllCustomCommands(customCommand.javaClass) + if (isCustomCommandInQueue(customCommand::class)) return executingNowError() val deferred = CompletableDeferred() add(CommandCustomCommand(aapsLogger, activePlugin, pumpEnactResultProvider::get, customCommand, object : Callback() { override fun run() { @@ -742,7 +742,7 @@ class CommandQueueImplementation @Inject constructor( } @Synchronized - override fun isCustomCommandInQueue(customCommandType: Class): Boolean { + override fun isCustomCommandInQueue(customCommandType: KClass): Boolean { if (isCustomCommandRunning(customCommandType)) { return true } @@ -757,23 +757,11 @@ class CommandQueueImplementation @Inject constructor( return false } - override fun isCustomCommandRunning(customCommandType: Class): Boolean { + override fun isCustomCommandRunning(customCommandType: KClass): Boolean { val performing = this.performing return performing is CommandCustomCommand && customCommandType.isInstance(performing.customCommand) } - @Synchronized - private fun removeAllCustomCommands(targetType: Class) { - synchronized(queue) { - for (i in queue.indices.reversed()) { - val command = queue[i] - if (command is CustomCommand && targetType.isInstance(command.commandType)) { - queue.removeAt(i) - } - } - } - } - /** * The running command in bold, then one queued command per line. * diff --git a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt index 7f3d5baa9acd..798fdb919e73 100644 --- a/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt +++ b/implementation/src/test/kotlin/app/aaps/implementation/queue/CommandQueueImplementationTest.kt @@ -507,13 +507,13 @@ class CommandQueueImplementationTest : TestBaseWithProfile() { commandQueue.pickup() // then - assertThat(commandQueue.isCustomCommandInQueue(CustomCommand1::class.java)).isTrue() - assertThat(commandQueue.isCustomCommandInQueue(CustomCommand2::class.java)).isTrue() - assertThat(commandQueue.isCustomCommandInQueue(CustomCommand3::class.java)).isFalse() + assertThat(commandQueue.isCustomCommandInQueue(CustomCommand1::class)).isTrue() + assertThat(commandQueue.isCustomCommandInQueue(CustomCommand2::class)).isTrue() + assertThat(commandQueue.isCustomCommandInQueue(CustomCommand3::class)).isFalse() - assertThat(commandQueue.isCustomCommandRunning(CustomCommand1::class.java)).isTrue() - assertThat(commandQueue.isCustomCommandRunning(CustomCommand2::class.java)).isFalse() - assertThat(commandQueue.isCustomCommandRunning(CustomCommand3::class.java)).isFalse() + assertThat(commandQueue.isCustomCommandRunning(CustomCommand1::class)).isTrue() + assertThat(commandQueue.isCustomCommandRunning(CustomCommand2::class)).isFalse() + assertThat(commandQueue.isCustomCommandRunning(CustomCommand3::class)).isFalse() assertThat(commandQueue.size()).isEqualTo(1) } diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt index 93746ae67add..cedc01b73349 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotunePlugin.kt @@ -62,7 +62,7 @@ import javax.inject.Singleton @Singleton class AutotunePlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, private val rxBus: RxBus, private val profileFunction: ProfileFunction, diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index 8e32ff4b0f10..198ca4dec4cf 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -111,7 +111,7 @@ class LoopPlugin @Inject constructor( private val preferences: Preferences, private val config: Config, private val constraintChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, private val context: Context, private val commandQueue: CommandQueue, @@ -716,14 +716,14 @@ class LoopPlugin @Inject constructor( private fun presentSuggestion(builder: NotificationCompat.Builder, contentText: String) { // Creates an explicit intent for an Activity in your app - val resultIntent = Intent(context, uiInteraction.mainActivity) + val resultIntent = Intent(context, uiInteraction.mainActivity.java) // The stack builder object will contain an artificial back stack for the // started Activity. // This ensures that navigating backward from the Activity leads out of // your application to the Home screen. val stackBuilder = TaskStackBuilder.create(context) - stackBuilder.addParentStack(uiInteraction.mainActivity) + stackBuilder.addParentStack(uiInteraction.mainActivity.java) // Adds the Intent that starts the Activity to the top of the stack stackBuilder.addNextIntent(resultIntent) val resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt index 569a623889fe..d9e311b16947 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt @@ -62,7 +62,7 @@ class OpenAPSAMAPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, private val constraintsChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val config: Config, private val profileFunction: ProfileFunction, private val activePlugin: ActivePlugin, diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt index d70a9461f91d..529f79347469 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt @@ -77,7 +77,7 @@ open class OpenAPSAutoISFPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, private val constraintsChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, private val config: Config, diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index 5e0149604330..8c0110bd0ffc 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -77,7 +77,7 @@ open class OpenAPSSMBPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, private val constraintsChecker: ConstraintsChecker, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, private val config: Config, diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt index 311df0254b65..622ccd9fc30c 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt @@ -251,7 +251,7 @@ class AutomationRuntime @Inject constructor( /** * Location permission is required only on a master device that has at least one enabled event - * using a [TriggerLocation]. Queried by [ActivePlugin.collectMissingPermissions] on every + * using a [TriggerLocation]. Queried by [app.aaps.core.interfaces.plugin.PluginPermissions.collectMissingPermissions] on every * collection pass, so the permission appears/disappears as the event set changes. */ override fun requiredPermissions(): List = diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionRunAutotune.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionRunAutotune.kt index c055eed5dda2..f031ed7079b3 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionRunAutotune.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionRunAutotune.kt @@ -92,5 +92,5 @@ class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) { return this } - override fun isValid(): Boolean = runBlocking { profileFunction.getProfile() } != null && activePlugin.getSpecificPluginsListByInterface(Autotune::class.java).first().isEnabled() + override fun isValid(): Boolean = runBlocking { profileFunction.getProfile() } != null && activePlugin.getSpecificPluginsListByInterface(Autotune::class).first().isEnabled() } \ No newline at end of file diff --git a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt index 1bdebe5a4d21..41e8047f1a1d 100644 --- a/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt +++ b/plugins/calibration/src/main/kotlin/app/aaps/plugins/calibration/LinearCalibrationPlugin.kt @@ -42,7 +42,7 @@ import kotlin.math.abs @Singleton class LinearCalibrationPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val dateUtil: DateUtil, private val persistenceLayer: PersistenceLayer, private val notificationManager: NotificationManager, diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt index 35b97f9df094..9c894fe1a254 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/configBuilder/ConfigBuilderImpl.kt @@ -258,14 +258,14 @@ class ConfigBuilderImpl @Inject constructor( override fun processOnEnabledCategoryChanged(changedPlugin: PluginBase, type: PluginType) { var pluginsInCategory: ArrayList? = null when { - type == PluginType.SENSITIVITY -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Sensitivity::class.java) - type == PluginType.SMOOTHING -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Smoothing::class.java) - type == PluginType.CALIBRATION -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Calibration::class.java) - type == PluginType.APS -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(APS::class.java) - type == PluginType.BGSOURCE -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(BgSource::class.java) - type == PluginType.PUMP -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Pump::class.java) + type == PluginType.SENSITIVITY -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Sensitivity::class) + type == PluginType.SMOOTHING -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Smoothing::class) + type == PluginType.CALIBRATION -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Calibration::class) + type == PluginType.APS -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(APS::class) + type == PluginType.BGSOURCE -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(BgSource::class) + type == PluginType.PUMP -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(Pump::class) // Process only NSClients - changedPlugin is NsClient -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(NsClient::class.java) + changedPlugin is NsClient -> pluginsInCategory = activePlugin.getSpecificPluginsListByInterface(NsClient::class) else -> { // do nothing } diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImpl.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImpl.kt index 5ddb807bbbb1..23ad2a835b04 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImpl.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImpl.kt @@ -25,7 +25,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isLoopInvocationAllowed(): Constraint = isLoopInvocationAllowed(ConstraintObject(true, aapsLogger)) override fun isLoopInvocationAllowed(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -37,7 +37,7 @@ class ConstraintsCheckerImpl @Inject constructor( override suspend fun isClosedLoopAllowed(): Constraint = isClosedLoopAllowed(ConstraintObject(true, aapsLogger)) override suspend fun isClosedLoopAllowed(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -49,7 +49,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isLgsForced(): Constraint = isLgsForced(ConstraintObject(false, aapsLogger)) override fun isLgsForced(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -61,7 +61,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isAutosensModeEnabled(): Constraint = isAutosensModeEnabled(ConstraintObject(true, aapsLogger)) override fun isAutosensModeEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -73,7 +73,7 @@ class ConstraintsCheckerImpl @Inject constructor( override suspend fun isSMBModeEnabled(): Constraint = isSMBModeEnabled(ConstraintObject(true, aapsLogger)) override suspend fun isSMBModeEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -85,7 +85,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isUAMEnabled(): Constraint = isUAMEnabled(ConstraintObject(true, aapsLogger)) override fun isUAMEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -97,7 +97,7 @@ class ConstraintsCheckerImpl @Inject constructor( override suspend fun isAdvancedFilteringEnabled(): Constraint = isAdvancedFilteringEnabled(ConstraintObject(true, aapsLogger)) override suspend fun isAdvancedFilteringEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -109,7 +109,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isSuperBolusEnabled(): Constraint = isSuperBolusEnabled(ConstraintObject(true, aapsLogger)) override fun isSuperBolusEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -123,7 +123,7 @@ class ConstraintsCheckerImpl @Inject constructor( override fun isConcentrationEnabled(): Constraint = isConcentrationEnabled(ConstraintObject(true, aapsLogger)) override fun applyBasalConstraints(absoluteRate: Constraint, profile: Profile): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -140,7 +140,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun applyBasalPercentConstraints(percentRate: Constraint, profile: Profile): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constrain = p as PluginConstraints if (!p.isEnabled()) continue @@ -150,7 +150,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun applyBolusConstraints(insulin: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constrain = p as PluginConstraints if (!p.isEnabled()) continue @@ -165,7 +165,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun applyExtendedBolusConstraints(insulin: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constrain = p as PluginConstraints if (!p.isEnabled()) continue @@ -180,7 +180,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun applyCarbsConstraints(carbs: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constrain = p as PluginConstraints if (!p.isEnabled()) continue @@ -190,7 +190,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override suspend fun applyMaxIOBConstraints(maxIob: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constrain = p as PluginConstraints if (!p.isEnabled()) continue @@ -200,7 +200,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun isAutomationEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue @@ -210,7 +210,7 @@ class ConstraintsCheckerImpl @Inject constructor( } override fun isConcentrationEnabled(value: Constraint): Constraint { - val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java) + val constraintsPlugins = activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class) for (p in constraintsPlugins) { val constraint = p as PluginConstraints if (!p.isEnabled()) continue diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt index efb3eca67d51..6dd242027ad5 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/bgQualityCheck/BgQualityCheckPlugin.kt @@ -34,7 +34,7 @@ import kotlin.math.min @Singleton class BgQualityCheckPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val rxBus: RxBus, private val iobCobCalculator: IobCobCalculator, private val dateUtil: DateUtil diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt index b3e64f3c42b7..cfc10efedf6b 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/ObjectivesPlugin.kt @@ -32,7 +32,7 @@ import javax.inject.Singleton @Singleton class ObjectivesPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, config: Config, val objectives: List<@JvmSuppressWildcards Objective> diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt index f4a2677fabe7..f49a449b7166 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/objectives/objectives/Objective0.kt @@ -31,7 +31,7 @@ class Objective0 @Inject constructor( private val passwordCheck: PasswordCheck, ) : Objective(preferences, rh, dateUtil, "config", R.string.objectives_0_objective, R.string.objectives_0_gate) { - val tidepoolPlugin get() = activePlugin.getSpecificPluginsListByInterface(Tidepool::class.java).firstOrNull() as Tidepool? + val tidepoolPlugin get() = activePlugin.getSpecificPluginsListByInterface(Tidepool::class).firstOrNull() as Tidepool? init { tasks.add(object : Task(this, R.string.objectives_bgavailableinns) { diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt index 7414e15e332f..c4a07564178c 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/safety/SafetyPlugin.kt @@ -39,7 +39,7 @@ import javax.inject.Singleton @Singleton class SafetyPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val preferences: Preferences, private val constraintChecker: ConstraintsChecker, private val activePlugin: ActivePlugin, diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt index d2235bbc6cff..a149f01b92b1 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/storage/StorageConstraintPlugin.kt @@ -23,7 +23,7 @@ import javax.inject.Singleton @Singleton class StorageConstraintPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val notificationManager: NotificationManager ) : PluginBase( PluginDescription() diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt index 453476299edf..4f0b4b6fe912 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/VersionCheckerPlugin.kt @@ -21,7 +21,7 @@ import javax.inject.Singleton @Singleton class VersionCheckerPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, private val versionCheckerUtils: VersionCheckerUtils, private val config: Config, diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt index 8309343333b4..beabd761f767 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/ConstraintsCheckerImplTest.kt @@ -200,7 +200,7 @@ class ConstraintsCheckerImplTest : TestBaseWithProfile() { // folded into the scan by ConstraintsCheckerImpl via activePumpInternal (stubbed per test). constraintsPluginsList.add(openAPSAMAPlugin) constraintsPluginsList.add(openAPSSMBPlugin) - whenever(activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class.java)).thenReturn(constraintsPluginsList) + whenever(activePlugin.getSpecificPluginsListByInterface(PluginConstraints::class)).thenReturn(constraintsPluginsList) } // Combo & Objectives diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index 03f88f8055d5..084e1106481a 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -61,7 +61,7 @@ import javax.inject.Singleton @Singleton class PersistentNotificationPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, private val fabricPrivacy: FabricPrivacy, @@ -180,10 +180,7 @@ class PersistentNotificationPlugin @Inject constructor( val basalIob = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().round() val cobInfo = iobCobCalculator.getCobInfo("PersistentNotificationPlugin") line2 = - rh.gs(app.aaps.core.ui.R.string.treatments_iob_label_string) + " " + rh.gs(R.string.notification_iob_short, bolusIob.iob + basalIob.basaliob) + " • " + rh.gs( - app.aaps.core.ui.R - .string.cob - ) + ": " + cobInfo.generateCOBString(decimalFormatter) + rh.gs(app.aaps.core.ui.R.string.treatments_iob_label_string) + " " + rh.gs(R.string.notification_iob_short, bolusIob.iob + basalIob.basaliob) + " • " + rh.gs(app.aaps.core.ui.R.string.cob) + ": " + cobInfo.generateCOBString(decimalFormatter) line3 = profileName /// For Android Auto val msgReadIntent = Intent() diff --git a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt index ea2f9eb852a7..bf227bb93fef 100644 --- a/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt +++ b/plugins/sensitivity/src/main/kotlin/app/aaps/plugins/sensitivity/SensitivityOref1Plugin.kt @@ -30,7 +30,7 @@ import kotlin.math.roundToInt @Singleton class SensitivityOref1Plugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, private val dateUtil: DateUtil ) : AbstractSensitivityPlugin( diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt index 64861d6333bd..e1673ddbe804 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/NSClientSourcePlugin.kt @@ -16,7 +16,7 @@ import javax.inject.Singleton @Singleton class NSClientSourcePlugin @Inject constructor( - rh: ResourceHelper, + override val rh: ResourceHelper, aapsLogger: AAPSLogger, config: Config, ) : PluginBase( diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index ebd5be421e5e..129144050fa4 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -130,7 +130,7 @@ import kotlin.time.Duration.Companion.milliseconds @Singleton class NSClientV3Plugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, private val rxBus: RxBus, private val context: Context, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt index f270e383b7a1..cc5d95c5d537 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/openhumans/OpenHumansUploaderPlugin.kt @@ -65,7 +65,7 @@ import app.aaps.core.ui.R as CoreUiR @Singleton class OpenHumansUploaderPlugin @Inject internal constructor( - rh: ResourceHelper, + override val rh: ResourceHelper, aapsLogger: AAPSLogger, preferences: Preferences, internal val context: Context, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index d677e79a13fc..a8ddd6abd81b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -113,7 +113,7 @@ import kotlin.math.min @Singleton class SmsCommunicatorPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val smsManager: SmsManager?, preferences: Preferences, private val constraintChecker: ConstraintsChecker, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index fb5b9ba194a5..24beb4b5bc3f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -50,7 +50,7 @@ import javax.inject.Singleton @Singleton class TizenPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, private val context: Context, private val dateUtil: DateUtil, private val rxBus: RxBus, diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 5ac2a9e39f2f..2c375db1864d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -78,7 +78,7 @@ import javax.inject.Singleton @Singleton class XdripPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, private val profileFunction: ProfileFunction, private val profileUtil: ProfileUtil, diff --git a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt index f4f79ef3cae7..0195a33f56df 100644 --- a/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt +++ b/pump/combov2/src/main/kotlin/info/nightscout/pump/combov2/ComboV2Plugin.kt @@ -116,7 +116,7 @@ internal const val PUMP_ERROR_TIMEOUT_INTERVAL_MSECS = 1000L * 60 * 5 @Singleton class ComboV2Plugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val context: Context, @@ -1620,7 +1620,7 @@ class ComboV2Plugin @Inject constructor( val description = when (val progStage = progressReport.stage) { is BasicProgressStage.EstablishingBtConnection -> rh.gs( - R.string.combov2_establishing_bt_connection, + TextRef.AndroidRes(R.string.combov2_establishing_bt_connection), progStage.currentAttemptNr ) @@ -1688,7 +1688,7 @@ class ComboV2Plugin @Inject constructor( val description = when (val stage = progressReport.stage) { is RTCommandProgressStage.DeliveringBolus -> rh.gs( - R.string.combov2_delivering_bolus, + TextRef.AndroidRes(R.string.combov2_delivering_bolus), stage.deliveredImmediateAmount.cctlBolusToIU(), stage.totalImmediateAmount.cctlBolusToIU() ) diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt index becdda8e2749..1a3d6677eea2 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/AbstractDanaRPlugin.kt @@ -63,7 +63,7 @@ import kotlin.math.max abstract class AbstractDanaRPlugin protected constructor( protected var danaPump: DanaPump, aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, protected val config: Config, commandQueue: CommandQueue, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt index 0af7396160e4..39b738aa3601 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danar/DanaRPlugin.kt @@ -53,7 +53,7 @@ import kotlin.math.max @Singleton class DanaRPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, config: Config, commandQueue: CommandQueue, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt index 6bf920f8a0d5..dd94050176c1 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarkorean/DanaRKoreanPlugin.kt @@ -54,7 +54,7 @@ class DanaRKoreanPlugin @Inject constructor( aapsLogger: AAPSLogger, rxBus: RxBus, private val context: Context, - rh: ResourceHelper, + override val rh: ResourceHelper, activePlugin: ActivePlugin, commandQueue: CommandQueue, danaPump: DanaPump, diff --git a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt index 99ce375349a7..5458f00dc29b 100644 --- a/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt +++ b/pump/danar/src/main/kotlin/app/aaps/pump/danarv2/DanaRv2Plugin.kt @@ -57,7 +57,7 @@ class DanaRv2Plugin @Inject constructor( aapsLogger: AAPSLogger, rxBus: RxBus, private val context: Context, - rh: ResourceHelper, + override val rh: ResourceHelper, activePlugin: ActivePlugin, commandQueue: CommandQueue, danaPump: DanaPump, diff --git a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt index 590e6aaae0ca..ab9992e38a61 100644 --- a/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt +++ b/pump/danars/src/main/kotlin/app/aaps/pump/danars/DanaRSPlugin.kt @@ -70,7 +70,7 @@ import kotlin.math.max @Singleton class DanaRSPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, diff --git a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt index dba03c64bd7a..88e8c2a77bdd 100644 --- a/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt +++ b/pump/diaconn/src/main/kotlin/app/aaps/pump/diaconn/DiaconnG8Plugin.kt @@ -70,7 +70,7 @@ import kotlin.math.max @Singleton class DiaconnG8Plugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, diff --git a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt index 0bc51336dcd9..07acd4d3a74e 100644 --- a/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt +++ b/pump/eopatch/src/main/kotlin/app/aaps/pump/eopatch/EopatchPumpPlugin.kt @@ -77,7 +77,7 @@ import kotlin.math.abs @Singleton class EopatchPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val aapsSchedulers: AapsSchedulers, diff --git a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt index 67cdc1488cd6..946226b13d13 100644 --- a/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt +++ b/pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt @@ -80,7 +80,7 @@ import javax.inject.Singleton @Singleton class EquilPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt index c586654ef40d..df9b4a359398 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightPlugin.kt @@ -140,7 +140,7 @@ import android.app.NotificationManager as AndroidNotificationManager @Singleton class InsightPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt index 1befed360c28..2b35a67052f0 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicPumpPlugin.kt @@ -119,7 +119,7 @@ import kotlin.math.floor @Singleton class MedtronicPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, rxBus: RxBus, diff --git a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt index 63f9f6c8d992..e23f6729eb92 100644 --- a/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt +++ b/pump/medtrum/src/main/kotlin/app/aaps/pump/medtrum/MedtrumPlugin.kt @@ -68,7 +68,7 @@ import kotlinx.coroutines.flow.drop @Singleton class MedtrumPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 36e96c2782ee..2f8fc4cfdd47 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -101,12 +101,11 @@ import javax.inject.Singleton import kotlin.concurrent.thread import kotlin.math.ceil import kotlin.time.Duration.Companion.hours -import app.aaps.core.interfaces.R as CoreInterfacesR @Singleton class OmnipodDashPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val omnipodManager: OmnipodDashManager, @@ -384,7 +383,7 @@ class OmnipodDashPumpPlugin @Inject constructor( } if (!podStateManager.alarmSynced) { podStateManager.alarmType?.let { - if (!commandQueue.isCustomCommandInQueue(CommandDeactivatePod::class.java)) { + if (!commandQueue.isCustomCommandInQueue(CommandDeactivatePod::class)) { showNotification( NotificationId.OMNIPOD_POD_FAULT, it.toString(), @@ -804,8 +803,6 @@ class OmnipodDashPumpPlugin @Inject constructor( // delivery not complete yet val remainingUnits = podStateManager.lastBolus!!.bolusUnitsRemaining val percent = ((requestedBolusAmount - remainingUnits) / requestedBolusAmount) * 100 - val delivered = requestedBolusAmount - remainingUnits - rh.gs(CoreInterfacesR.string.bolus_delivering, delivered) bolusProgressData.updateProgress(percent = percent.toInt()) val sleepSeconds = if (bolusCanceled) @@ -1078,7 +1075,7 @@ class OmnipodDashPumpPlugin @Inject constructor( aapsLogger.warn(LTag.PUMP, "Unsupported custom command: " + customCommand.javaClass.name) pumpEnactResultProvider.get().success(false).enacted(false).comment( rh.gs( - app.aaps.pump.omnipod.common.R.string.omnipod_common_error_unsupported_custom_command, + TextRef.AndroidRes(app.aaps.pump.omnipod.common.R.string.omnipod_common_error_unsupported_custom_command), customCommand.javaClass.name ) ) @@ -1413,7 +1410,7 @@ class OmnipodDashPumpPlugin @Inject constructor( podStateManager.tempBasal = command.tempBasal // Evaluate basal drift correction after confirmed temp basal set. - if (!commandQueue.isCustomCommandInQueue(CommandDeliverBasalCorrection::class.java) && + if (!commandQueue.isCustomCommandInQueue(CommandDeliverBasalCorrection::class) && podStateManager.needsBasalCorrection() ) { // Queue-worker deadlock guard — don't unwrap the .launch. See CommandQueue kdoc. diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt index cba4f4debe20..1a5bf1e3f2a6 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/DashOverviewViewModel.kt @@ -331,14 +331,14 @@ class DashOverviewViewModel @Inject constructor( label = rh.gs(CommonR.string.omnipod_common_overview_button_silence_alerts), icon = Icons.Filled.NotificationsOff, enabled = queueEmpty, - visible = podStateManager.isPodRunning && (podStateManager.activeAlerts?.isNotEmpty() == true || commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class.java)), + visible = podStateManager.isPodRunning && (podStateManager.activeAlerts?.isNotEmpty() == true || commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class)), onClick = { runCustomCommandWithErrorDialog(CommandSilenceAlerts(), rh.gs(CommonR.string.omnipod_common_error_failed_to_silence_alerts)) } ), PumpAction( label = rh.gs(CommonR.string.omnipod_common_overview_button_resume_delivery), icon = Icons.Filled.PlayArrow, enabled = queueEmpty, - visible = podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class.java)), + visible = podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class)), onClick = { runCustomCommandWithErrorDialog(CommandResumeDelivery(), rh.gs(CommonR.string.omnipod_common_error_failed_to_resume_delivery)) } ), PumpAction( @@ -380,7 +380,7 @@ class DashOverviewViewModel @Inject constructor( label = rh.gs(CommonR.string.omnipod_common_pod_management_button_play_test_beep), icon = Icons.AutoMirrored.Filled.VolumeUp, category = ActionCategory.MANAGEMENT, - enabled = podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && !commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class.java), + enabled = podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED) && !commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class), visible = podStateManager.activationProgress.isAtLeast(ActivationProgress.PHASE_1_COMPLETED), onClick = { runCustomCommandWithErrorDialog(CommandPlayTestBeep(), rh.gs(CommonR.string.omnipod_common_error_failed_to_play_test_beep)) } ), diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt index 011e0aec7bd4..986921f848db 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/OmnipodErosPumpPlugin.kt @@ -128,7 +128,7 @@ import javax.inject.Singleton @Singleton class OmnipodErosPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val rxBus: RxBus, @@ -206,7 +206,7 @@ class OmnipodErosPumpPlugin @Inject constructor( if (this@OmnipodErosPumpPlugin.hasTimeDateOrTimeZoneChanged) pluginScope.launch { commandQueue.customCommand(CommandHandleTimeChange(false)) } if (!this@OmnipodErosPumpPlugin.verifyPodAlertConfiguration()) pluginScope.launch { commandQueue.customCommand(CommandUpdateAlertConfiguration()) } if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled && podStateManager.isPodActivationCompleted && - !podStateManager.isPodDead && podStateManager.activeAlerts.size() > 0 && !commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class.java) + !podStateManager.isPodDead && podStateManager.activeAlerts.size() > 0 && !commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class) ) queueAcknowledgeAlertsCommand() } else aapsLogger.debug(LTag.PUMP, "Skipping Pod status check because command queue is not empty") @@ -373,7 +373,7 @@ class OmnipodErosPumpPlugin @Inject constructor( notificationManager.post(NotificationId.OMNIPOD_POD_ALERTS, notificationText) runBlocking { pumpSync.insertAnnouncement(notificationText, null, PumpType.OMNIPOD_EROS, serialNumber()) } - if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled && !commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class.java)) { + if (aapsOmnipodErosManager.isAutomaticallyAcknowledgeAlertsEnabled && !commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class)) { queueAcknowledgeAlertsCommand() } } diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt index 64d10110a56c..699c16dd4e8b 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/ErosOverviewViewModel.kt @@ -297,28 +297,28 @@ class ErosOverviewViewModel @Inject constructor( label = rh.gs(CommonR.string.omnipod_common_overview_button_silence_alerts), icon = Icons.Filled.NotificationsOff, enabled = rlReady && queueEmpty, - visible = !omnipodManager.isAutomaticallyAcknowledgeAlertsEnabled && podStateManager.isPodRunning && (podStateManager.hasActiveAlerts() || commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class.java)), + visible = !omnipodManager.isAutomaticallyAcknowledgeAlertsEnabled && podStateManager.isPodRunning && (podStateManager.hasActiveAlerts() || commandQueue.isCustomCommandInQueue(CommandSilenceAlerts::class)), onClick = { runCustomCommandWithErrorDialog(CommandSilenceAlerts(), rh.gs(CommonR.string.omnipod_common_error_failed_to_silence_alerts)) } ), PumpAction( label = rh.gs(CommonR.string.omnipod_common_overview_button_resume_delivery), icon = Icons.Filled.PlayArrow, enabled = rlReady && queueEmpty, - visible = podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class.java)), + visible = podStateManager.isPodRunning && (podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandResumeDelivery::class)), onClick = { runCustomCommandWithErrorDialog(CommandResumeDelivery(), rh.gs(CommonR.string.omnipod_common_error_failed_to_resume_delivery)) } ), PumpAction( label = rh.gs(CommonR.string.omnipod_common_overview_button_suspend_delivery), icon = Icons.Filled.Pause, enabled = podStateManager.isPodRunning && !podStateManager.isSuspended && rlReady && queueEmpty, - visible = omnipodManager.isSuspendDeliveryButtonEnabled && podStateManager.isPodRunning && (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class.java)), + visible = omnipodManager.isSuspendDeliveryButtonEnabled && podStateManager.isPodRunning && (!podStateManager.isSuspended || commandQueue.isCustomCommandInQueue(CommandSuspendDelivery::class)), onClick = { runCustomCommandWithErrorDialog(CommandSuspendDelivery(), rh.gs(CommonR.string.omnipod_common_error_failed_to_suspend_delivery)) } ), PumpAction( label = rh.gs(CommonR.string.omnipod_common_overview_button_set_time), icon = Icons.Filled.Schedule, enabled = podStateManager.isPodRunning && !podStateManager.isSuspended && rlReady && queueEmpty, - visible = podStateManager.isPodRunning && (podStateManager.timeDeviatesMoreThan(Duration.standardMinutes(5)) || commandQueue.isCustomCommandInQueue(CommandHandleTimeChange::class.java)), + visible = podStateManager.isPodRunning && (podStateManager.timeDeviatesMoreThan(Duration.standardMinutes(5)) || commandQueue.isCustomCommandInQueue(CommandHandleTimeChange::class)), onClick = { runCustomCommandWithErrorDialog(CommandHandleTimeChange(true), rh.gs(CommonR.string.omnipod_common_error_failed_to_set_time)) } ) ) @@ -348,7 +348,7 @@ class ErosOverviewViewModel @Inject constructor( label = rh.gs(CommonR.string.omnipod_common_pod_management_button_play_test_beep), icon = Icons.AutoMirrored.Filled.VolumeUp, category = ActionCategory.MANAGEMENT, - enabled = rlReady && !commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class.java), + enabled = rlReady && !commandQueue.isCustomCommandInQueue(CommandPlayTestBeep::class), visible = podStateManager.isPodInitialized && podStateManager.activationProgress.isAtLeast(ActivationProgress.PAIRING_COMPLETED), onClick = { runCustomCommandWithErrorDialog(CommandPlayTestBeep(), rh.gs(CommonR.string.omnipod_common_error_failed_to_play_test_beep)) } ), diff --git a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt index 1d889547ec3e..954a754a5ade 100644 --- a/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt +++ b/pump/virtual/src/main/kotlin/app/aaps/pump/virtual/VirtualPumpPlugin.kt @@ -62,7 +62,7 @@ import javax.inject.Singleton open class VirtualPumpPlugin @Inject constructor( aapsLogger: AAPSLogger, private val rxBus: RxBus, - rh: ResourceHelper, + override val rh: ResourceHelper, preferences: Preferences, commandQueue: CommandQueue, private val pumpSync: PumpSync, diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceViewModel.kt index 6b28f4b2b023..fd49d01beb5f 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceViewModel.kt @@ -203,7 +203,7 @@ class MaintenanceViewModel @Inject constructor( try { withContext(Dispatchers.IO) { persistenceLayer.clearDatabases() - for (plugin in activePlugin.getSpecificPluginsListByInterface(OwnDatabasePlugin::class.java)) { + for (plugin in activePlugin.getSpecificPluginsListByInterface(OwnDatabasePlugin::class)) { (plugin as OwnDatabasePlugin).clearAllTables() } nsClient.dataSyncSelector.resetToNextFullSync() diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModel.kt index de8ebd6facb1..32b1f73e5bfa 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModel.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.plugin.PermissionGroup +import app.aaps.core.interfaces.plugin.PluginPermissions import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow @@ -20,7 +20,7 @@ import javax.inject.Inject @Stable class PermissionsViewModel @Inject constructor( @ApplicationContext private val context: Context, - private val activePlugin: ActivePlugin, + private val pluginPermissions: PluginPermissions, ) : ViewModel() { private val _uiState = MutableStateFlow(PermissionsUiState()) @@ -29,8 +29,8 @@ class PermissionsViewModel @Inject constructor( val sideEffect: SharedFlow = _sideEffect fun refresh() { - val allGroups = activePlugin.collectAllPermissions(context) - val missingGroups = activePlugin.collectMissingPermissions(context) + val allGroups = pluginPermissions.collectAllPermissions(context) + val missingGroups = pluginPermissions.collectMissingPermissions(context) val missingPermSets = missingGroups.map { it.permissions.toSet() }.toSet() val items = allGroups.map { group -> diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/preferences/AllPreferencesScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/preferences/AllPreferencesScreen.kt index 07a528376e9e..e85501833172 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/preferences/AllPreferencesScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/preferences/AllPreferencesScreen.kt @@ -68,7 +68,7 @@ fun AllPreferencesScreen( val preferences = LocalPreferences.current val config = LocalConfig.current // Look up plugins by interface - val autotunePlugin = activePlugin.getSpecificPluginsListByInterface(Autotune::class.java).firstOrNull() + val autotunePlugin = activePlugin.getSpecificPluginsListByInterface(Autotune::class).firstOrNull() // Built-in preference screens from BuiltInSearchables (single source of truth) val generalPreferences = builtInSearchables.general diff --git a/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModelTest.kt b/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModelTest.kt index 43e9aca3159a..af981df4bc5e 100644 --- a/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModelTest.kt +++ b/ui/src/test/kotlin/app/aaps/ui/compose/permissionsSheet/PermissionsViewModelTest.kt @@ -1,7 +1,7 @@ package app.aaps.ui.compose.permissionsSheet import android.content.Context -import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.plugin.PluginPermissions import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -20,7 +20,7 @@ import org.mockito.kotlin.whenever internal class PermissionsViewModelTest { @Mock private lateinit var context: Context - @Mock private lateinit var activePlugin: ActivePlugin + @Mock private lateinit var pluginPermissions: PluginPermissions private lateinit var sut: PermissionsViewModel @@ -30,7 +30,7 @@ internal class PermissionsViewModelTest { // requestPermission()/onPermissionsDenied() use viewModelScope; setMain keeps those deferred. // Construction reads nothing from deps (no init block), so no stubbing is required to build. Dispatchers.setMain(StandardTestDispatcher()) - sut = PermissionsViewModel(context, activePlugin) + sut = PermissionsViewModel(context, pluginPermissions) } @AfterEach @@ -59,8 +59,8 @@ internal class PermissionsViewModelTest { @Test fun `refresh with no permissions produces empty granted state`() { - whenever(activePlugin.collectAllPermissions(any())).thenReturn(emptyList()) - whenever(activePlugin.collectMissingPermissions(any())).thenReturn(emptyList()) + whenever(pluginPermissions.collectAllPermissions(any())).thenReturn(emptyList()) + whenever(pluginPermissions.collectMissingPermissions(any())).thenReturn(emptyList()) sut.refresh() From 6b64ba9400eab6418746c92aa01ab4852e137f02 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 10:34:09 +0200 Subject: [PATCH 116/146] :core:ui becomes a multiplatform module Same four plugin recipe as :core:interfaces - kotlin("multiplatform") plus the AGP multiplatform library, the Compose compiler and Compose Multiplatform. src/main becomes src/androidMain and src/test becomes src/androidHostTest. 166 of 438 files reach commonMain, 0 before. 131 of them are icon definitions, moved untouched: CMP reuses the androidx package names, so not one import changed. The rest came from compiling for iosArm64 and moving back whatever failed, three rounds to a fixpoint. What stays on Android, and why: - 174 *Previews.kt files. androidx.compose.ui.tooling.preview.Preview is Android only and CMP keeps its own in a separate artifact. Splitting each icon from its preview harness costs nothing, because androidMain sees commonMain. - ~95 files that name an Android resource id. R appears 75 times in the root blocker list, ResourceHelper.gs 30 more. UiStrings is already generated for this module, so the way out is TextRef.Named, the same conversion :core:keys and PluginDescription went through. Not started here. - 16 files that use a real Android API - Context, LocalContext, fromHtml, FragmentActivity, window and requestedOrientation. Restated by hand, because android-module-dependencies, test-module-dependencies, compose-test-module-dependencies and jacoco-module-dependencies all apply com.android.library and a multiplatform module cannot: - lint MissingTranslation / ExtraTranslation off, or the incomplete locale files would fail a release build for the first time here. - withHostTest { isIncludeAndroidResources = true; isReturnDefaultValues = true }. This one is load bearing: without it 93 of the 115 Robolectric Compose tests fail, because createComposeRule() has no merged resource table and no manifest holding the activity it launches. - The JaCoCo isIncludeNoLocationClasses flag. Robolectric rewrites bytecode in its own classloader, so without it the agent records nothing for the Compose screens the tests drive. Coverage data for this module goes from 191 KB to 655 KB with it back. The jacoco plugin itself is applied to every project by the root build file, and jacocoAllDebugReport already knows how to read a multiplatform module, so the aggregate report keeps working. androidx-compose-ui-tooling moves from debugImplementation to implementation: the AGP multiplatform library target has no build types, so there is no debug only configuration. R8 in the app module drops it from a release build. Note for anyone verifying locally: testFullDebugUnitTest does not exist for a multiplatform module. Use testAndroidHostTest, as CircleCI already does. Also in here, because the KMP move rewrote every path in this module and the two cannot be separated into buildable commits: display formatting leaves :core:objects. TrendArrow.directionToIcon(), CobInfo.generateCOBString()/displayText() and the TT display helpers now live in :core:ui, next to the strings and icons they use. A domain module was reaching into the UI module to pick a picture for an enum and to name a format string. TT.target() stays behind, because it is arithmetic. This does not remove the :core:objects -> :core:ui dependency. TB.toStringFull and EB.toStringFull call domain helpers used across plugins/aps, implementation and pump/*, so moving them would need :core:ui -> :core:objects, which cycles while ProfileSealed, BolusWizard and RunningModeGuard still name core.ui strings. Those three have to be cleaned first. --- .../extensions/GlucoseValueExtension.kt | 24 --- .../extensions/TemporaryTargetExtension.kt | 19 -- core/ui/build.gradle.kts | 195 +++++++++++++----- .../app/aaps/core/ui/compose/AapsCardTest.kt | 0 .../app/aaps/core/ui/compose/AapsFabTest.kt | 0 .../core/ui/compose/AapsSearchFieldTest.kt | 0 .../core/ui/compose/QuickAddButtonsTest.kt | 0 .../app/aaps/core/ui/compose/TonalIconTest.kt | 0 .../ui/compose/dialogs/DatePickerModalTest.kt | 0 .../ui/compose/dialogs/ErrorDialogTest.kt | 0 .../compose/dialogs/GlobalDialogHostTest.kt | 0 .../compose/dialogs/GlobalSnackbarHostTest.kt | 0 .../ui/compose/dialogs/OkCancelDialogTest.kt | 0 .../core/ui/compose/dialogs/OkDialogTest.kt | 0 .../dialogs/QueryAnyPasswordDialogTest.kt | 0 .../dialogs/QueryPasswordDialogTest.kt | 0 .../compose/dialogs/SetPasswordDialogTest.kt | 0 .../compose/dialogs/ThreeButtonDialogTest.kt | 0 .../ui/compose/dialogs/TimePickerModalTest.kt | 0 .../compose/dialogs/UnifiedAuthDialogTest.kt | 0 .../compose/dialogs/ValueInputDialogTest.kt | 0 .../compose/dialogs/YesNoCancelDialogTest.kt | 0 .../navigation/ElementTypeStyleTest.kt | 0 .../ui/compose/navigation/ElementTypeTest.kt | 0 .../preference/AdaptivePreferenceItemsTest.kt | 0 .../MorePreferenceComponentsTest.kt | 0 .../preference/PreferenceComponentsTest.kt | 0 .../preference/PreferenceSheetContentTest.kt | 0 .../ui/compose/pump/PumpOverviewScreenTest.kt | 0 .../siteRotation/BodyViewContainsPointTest.kt | 0 .../app/aaps/core/ui/elements/WeekDayTest.kt | 0 .../{main => androidMain}/AndroidManifest.xml | 0 .../app/aaps/core/ui/AlarmSoundResources.kt | 0 .../ui/clientcontrol/FailureReasonText.kt | 0 .../aaps/core/ui/compose/AapsCardPreviews.kt | 0 .../aaps/core/ui/compose/AapsFabPreviews.kt | 0 .../aaps/core/ui/compose/AapsSearchField.kt | 0 .../ui/compose/AapsSearchFieldPreviews.kt | 0 .../app/aaps/core/ui/compose/AapsTheme.kt | 0 .../core/ui/compose/AapsTopAppBarPreviews.kt | 0 .../aaps/core/ui/compose/AapsTypography.kt | 0 .../app/aaps/core/ui/compose/CarbTimeRow.kt | 0 .../aaps/core/ui/compose/ConfigPluginCard.kt | 0 .../aaps/core/ui/compose/DateTimeSection.kt | 0 .../app/aaps/core/ui/compose/EventTimeRow.kt | 0 .../app/aaps/core/ui/compose/FormatUtils.kt | 0 .../ui/compose/ImportSummaryComponents.kt | 0 .../aaps/core/ui/compose/InsulinSelector.kt | 0 .../core/ui/compose/MasterOfflineBanner.kt | 0 .../aaps/core/ui/compose/NumberInputRow.kt | 0 .../core/ui/compose/NumberInputRowPreviews.kt | 0 .../core/ui/compose/PluginCategoryTitle.kt | 0 .../app/aaps/core/ui/compose/PlusMinusEdit.kt | 0 .../aaps/core/ui/compose/ProtectionHost.kt | 0 .../ui/compose/QuickAddButtonsPreviews.kt | 0 .../core/ui/compose/SelectableListToolbar.kt | 0 .../aaps/core/ui/compose/SliderWithButtons.kt | 0 .../ui/compose/SliderWithButtonsPreviews.kt | 0 .../app/aaps/core/ui/compose/StatusLevel.kt | 0 .../aaps/core/ui/compose/TextRefResource.kt | 0 .../aaps/core/ui/compose/TimeRangePicker.kt | 0 .../app/aaps/core/ui/compose/UnitTypeText.kt | 0 .../core/ui/compose/banner/BannerPreviews.kt | 0 .../ui/compose/dialogs/ConfirmationMessage.kt | 0 .../ui/compose/dialogs/DatePickerModal.kt | 0 .../dialogs/DatePickerModalPreviews.kt | 0 .../dialogs/ElementConfirmationDialog.kt | 0 .../core/ui/compose/dialogs/ErrorDialog.kt | 0 .../ui/compose/dialogs/ErrorDialogPreviews.kt | 0 .../ui/compose/dialogs/GlobalDialogHost.kt | 0 .../ui/compose/dialogs/GlobalSnackbarHost.kt | 0 .../core/ui/compose/dialogs/OkCancelDialog.kt | 0 .../compose/dialogs/OkCancelDialogPreviews.kt | 0 .../aaps/core/ui/compose/dialogs/OkDialog.kt | 0 .../ui/compose/dialogs/OkDialogPreviews.kt | 0 .../compose/dialogs/QueryAnyPasswordDialog.kt | 0 .../dialogs/QueryAnyPasswordDialogPreviews.kt | 0 .../ui/compose/dialogs/QueryPasswordDialog.kt | 0 .../dialogs/QueryPasswordDialogPreviews.kt | 0 .../ui/compose/dialogs/SetPasswordDialog.kt | 0 .../dialogs/SetPasswordDialogPreviews.kt | 0 .../ui/compose/dialogs/ThreeButtonDialog.kt | 0 .../dialogs/ThreeButtonDialogPreviews.kt | 0 .../ui/compose/dialogs/TimePickerModal.kt | 0 .../dialogs/TimePickerModalPreviews.kt | 0 .../ui/compose/dialogs/UnifiedAuthDialog.kt | 0 .../ui/compose/dialogs/ValueInputDialog.kt | 0 .../dialogs/ValueInputDialogPreviews.kt | 0 .../ui/compose/dialogs/YesNoCancelDialog.kt | 0 .../dialogs/YesNoCancelDialogPreviews.kt | 0 .../ui/compose/icons/CareportalPreviews.kt | 0 .../core/ui/compose/icons/IcAapsPreviews.kt | 0 .../core/ui/compose/icons/IcActionPreviews.kt | 0 .../ui/compose/icons/IcActivityPreviews.kt | 0 .../compose/icons/IcAnnouncementPreviews.kt | 0 .../icons/IcArrowDoubleDownPreviews.kt | 0 .../compose/icons/IcArrowDoubleUpPreviews.kt | 0 .../ui/compose/icons/IcArrowFlatPreviews.kt | 0 .../icons/IcArrowFortyFiveDownPreviews.kt | 0 .../icons/IcArrowFortyFiveUpPreviews.kt | 0 .../compose/icons/IcArrowInvalidPreviews.kt | 0 .../compose/icons/IcArrowLeftDownPreviews.kt | 0 .../ui/compose/icons/IcArrowLeftPreviews.kt | 0 .../ui/compose/icons/IcArrowLeftUpPreviews.kt | 0 .../ui/compose/icons/IcArrowNonePreviews.kt | 0 .../icons/IcArrowSimpleDownPreviews.kt | 0 .../compose/icons/IcArrowSimpleUpPreviews.kt | 0 .../ui/compose/icons/IcAsAbovePreviews.kt | 0 .../ui/compose/icons/IcAsAboveXPreviews.kt | 0 .../ui/compose/icons/IcAsBelowPreviews.kt | 0 .../ui/compose/icons/IcAsBelowXPreviews.kt | 0 .../core/ui/compose/icons/IcAsPreviews.kt | 0 .../core/ui/compose/icons/IcAsXPreviews.kt | 0 .../ui/compose/icons/IcAutomationPreviews.kt | 0 .../ui/compose/icons/IcBgCheckPreviews.kt | 0 .../core/ui/compose/icons/IcBolusPreviews.kt | 0 .../core/ui/compose/icons/IcBreadPreviews.kt | 0 .../core/ui/compose/icons/IcByodaPreviews.kt | 0 .../core/ui/compose/icons/IcCakePreviews.kt | 0 .../ui/compose/icons/IcCalculatorPreviews.kt | 0 .../ui/compose/icons/IcCalibrationPreviews.kt | 0 .../icons/IcCancelExtendedBolusPreviews.kt | 0 .../compose/icons/IcCannulaChangePreviews.kt | 0 .../core/ui/compose/icons/IcCarbsPreviews.kt | 0 .../ui/compose/icons/IcCgmInsertPreviews.kt | 0 .../compose/icons/IcClinicalNotesPreviews.kt | 0 .../icons/IcCompareProfilesPreviews.kt | 0 .../core/ui/compose/icons/IcDeltaPreviews.kt | 0 .../ui/compose/icons/IcDiaconnPreviews.kt | 0 .../compose/icons/IcExtendedBolusPreviews.kt | 0 .../ui/compose/icons/IcGenericCgmPreviews.kt | 0 .../ui/compose/icons/IcGenericIconPreviews.kt | 0 .../ui/compose/icons/IcGoogleDrivePreviews.kt | 0 .../ui/compose/icons/IcHistoryPreviews.kt | 0 .../ui/compose/icons/IcLoopClosedPreviews.kt | 0 .../compose/icons/IcLoopDisabledPreviews.kt | 0 .../icons/IcLoopDisconnectedPreviews.kt | 0 .../ui/compose/icons/IcLoopHiddenPreviews.kt | 0 .../ui/compose/icons/IcLoopLgsPreviews.kt | 0 .../ui/compose/icons/IcLoopOpenPreviews.kt | 0 .../compose/icons/IcLoopPausedDstPreviews.kt | 0 .../ui/compose/icons/IcLoopPausedPreviews.kt | 0 .../compose/icons/IcLoopPausedPumpPreviews.kt | 0 .../compose/icons/IcLoopReconnectPreviews.kt | 0 .../compose/icons/IcLoopSuperBolusPreviews.kt | 0 .../core/ui/compose/icons/IcMdiPreviews.kt | 0 .../core/ui/compose/icons/IcNoTbrPreviews.kt | 0 .../core/ui/compose/icons/IcNotePreviews.kt | 0 .../ui/compose/icons/IcPatchPumpPreviews.kt | 0 .../core/ui/compose/icons/IcPizzaPreviews.kt | 0 .../icons/IcPluginAutomationPreviews.kt | 0 .../compose/icons/IcPluginAutotunePreviews.kt | 0 .../ui/compose/icons/IcPluginByodaPreviews.kt | 0 .../ui/compose/icons/IcPluginComboPreviews.kt | 0 .../icons/IcPluginConfigBuilderPreviews.kt | 0 .../ui/compose/icons/IcPluginDanaPreviews.kt | 0 .../compose/icons/IcPluginDiaconnPreviews.kt | 0 .../compose/icons/IcPluginEopatchPreviews.kt | 0 .../ui/compose/icons/IcPluginEquilPreviews.kt | 0 .../icons/IcPluginEversensePreviews.kt | 0 .../ui/compose/icons/IcPluginFoodPreviews.kt | 0 .../compose/icons/IcPluginGarminPreviews.kt | 0 .../ui/compose/icons/IcPluginGlimpPreviews.kt | 0 .../compose/icons/IcPluginGlunovoPreviews.kt | 0 .../compose/icons/IcPluginInsightPreviews.kt | 0 .../compose/icons/IcPluginInsulinPreviews.kt | 0 .../icons/IcPluginIntelligoPreviews.kt | 0 .../icons/IcPluginMaintenancePreviews.kt | 0 .../icons/IcPluginMedtronicPreviews.kt | 0 .../compose/icons/IcPluginMedtrumPreviews.kt | 0 .../compose/icons/IcPluginMm640GPreviews.kt | 0 .../icons/IcPluginNsClientBgPreviews.kt | 0 .../compose/icons/IcPluginNsClientPreviews.kt | 0 .../icons/IcPluginObjectivesPreviews.kt | 0 .../compose/icons/IcPluginOmnipodPreviews.kt | 0 .../compose/icons/IcPluginOpenApsPreviews.kt | 0 .../icons/IcPluginOpenHumansPreviews.kt | 0 .../compose/icons/IcPluginPocTechPreviews.kt | 0 .../compose/icons/IcPluginRandomBgPreviews.kt | 0 .../ui/compose/icons/IcPluginSmsPreviews.kt | 0 .../ui/compose/icons/IcPluginSyaiPreviews.kt | 0 .../ui/compose/icons/IcPluginTMobiPreviews.kt | 0 .../compose/icons/IcPluginTidepoolPreviews.kt | 0 .../ui/compose/icons/IcPluginTizenPreviews.kt | 0 .../compose/icons/IcPluginTomatoPreviews.kt | 0 .../icons/IcPluginVirtualPumpPreviews.kt | 0 .../ui/compose/icons/IcProfilePreviews.kt | 0 .../ui/compose/icons/IcPumpBatteryPreviews.kt | 0 .../compose/icons/IcPumpCartridgePreviews.kt | 0 .../ui/compose/icons/IcQuestionPreviews.kt | 0 .../ui/compose/icons/IcQuickWizardPreviews.kt | 0 .../ui/compose/icons/IcSettingsOffPreviews.kt | 0 .../ui/compose/icons/IcSetupWizardPreviews.kt | 0 .../compose/icons/IcSiteRotationPreviews.kt | 0 .../core/ui/compose/icons/IcSmbPreviews.kt | 0 .../core/ui/compose/icons/IcStatsPreviews.kt | 0 .../ui/compose/icons/IcTbrCancelPreviews.kt | 0 .../ui/compose/icons/IcTbrHighPreviews.kt | 0 .../core/ui/compose/icons/IcTbrLowPreviews.kt | 0 .../ui/compose/icons/IcTtActivityPreviews.kt | 0 .../ui/compose/icons/IcTtCancelPreviews.kt | 0 .../compose/icons/IcTtEatingSoonPreviews.kt | 0 .../core/ui/compose/icons/IcTtHighPreviews.kt | 0 .../core/ui/compose/icons/IcTtHypoPreviews.kt | 0 .../ui/compose/icons/IcTtManualPreviews.kt | 0 .../ui/compose/icons/IcUserOptionsPreviews.kt | 0 .../core/ui/compose/icons/IcXDripPreviews.kt | 0 .../aaps/core/ui/compose/icons/NsPreviews.kt | 0 .../core/ui/compose/icons/PumpPreviews.kt | 0 .../icons/library/IcChildBackPreviews.kt | 0 .../icons/library/IcChildFrontPreviews.kt | 0 .../icons/library/IcManBackPreviews.kt | 0 .../icons/library/IcManFrontPreviews.kt | 0 .../icons/library/IcWomanBackPreviews.kt | 0 .../icons/library/IcWomanFrontPreviews.kt | 0 .../unused/IcActivityTreatmentsPreviews.kt | 0 .../library/unused/IcArrowCenterPreviews.kt | 0 .../library/unused/IcArrowFlatPreviews.kt | 0 .../library/unused/IcPluginActionPreviews.kt | 0 .../unused/IcPluginConfigBuilderPreviews.kt | 0 .../unused/IcPluginOverviewPreviews.kt | 0 .../compose/insulin/ConcentrationDropDown.kt | 0 .../core/ui/compose/insulin/SelectInsulin.kt | 0 .../compose/insulin/SelectInsulinPreviews.kt | 0 .../ui/compose/navigation/ElementTypeStyle.kt | 0 .../ui/compose/pickers/HourWheelPicker.kt | 0 .../ui/compose/pickers/WeekDaySelector.kt | 0 .../pickers/WeekDaySelectorPreviews.kt | 0 .../preference/AdaptiveDoublePreference.kt | 0 .../AdaptiveDoublePreferencePreviews.kt | 0 .../preference/AdaptiveIntPreference.kt | 0 .../AdaptiveIntPreferencePreviews.kt | 0 .../preference/AdaptiveIntentPreference.kt | 0 .../preference/AdaptiveListPreference.kt | 0 .../AdaptiveListPreferencePreviews.kt | 0 .../AdaptiveMasterPasswordPreference.kt | 0 ...daptiveMasterPasswordPreferencePreviews.kt | 0 .../preference/AdaptivePasswordPreference.kt | 0 .../AdaptivePasswordPreferencePreviews.kt | 0 .../preference/AdaptivePreferenceItem.kt | 0 .../preference/AdaptivePreferenceList.kt | 0 .../preference/AdaptiveStringPreference.kt | 0 .../AdaptiveStringPreferencePreviews.kt | 0 .../preference/AdaptiveSwitchPreference.kt | 0 .../AdaptiveSwitchPreferencePreviews.kt | 0 .../AdaptiveUnitDoublePreference.kt | 0 .../ClickablePreferenceCategoryHeader.kt | 0 .../CollapsibleCardSectionContent.kt | 0 .../CollapsibleCardSectionContentPreviews.kt | 0 .../preference/InlinePreferenceItems.kt | 0 .../ui/compose/preference/ListPreference.kt | 0 .../preference/ListPreferencePreviews.kt | 0 .../preference/PluginPreferencesScreen.kt | 0 .../core/ui/compose/preference/Preference.kt | 0 .../preference/PreferenceAlertDialog.kt | 0 .../compose/preference/PreferenceCategory.kt | 0 .../preference/PreferenceCategoryPreviews.kt | 0 .../preference/PreferenceContentExtensions.kt | 0 .../compose/preference/PreferencePreviews.kt | 0 .../preference/PreferenceSheetContent.kt | 0 .../preference/PreferenceSliderWithButtons.kt | 0 .../ui/compose/preference/PreferenceState.kt | 0 .../ui/compose/preference/PreferenceTheme.kt | 0 .../ui/compose/preference/PreviewUtils.kt | 0 .../ui/compose/preference/SwitchPreference.kt | 0 .../preference/SwitchPreferencePreviews.kt | 0 .../core/ui/compose/preference/SyncBadge.kt | 0 .../compose/preference/TextFieldPreference.kt | 0 .../preference/TextFieldPreferencePreviews.kt | 0 .../core/ui/compose/pump/BlePreCheckHost.kt | 0 .../aaps/core/ui/compose/pump/BleScanStep.kt | 0 .../compose/pump/BluetoothPermissionsHost.kt | 0 .../ui/compose/pump/KeepScreenOnEffect.kt | 0 .../ui/compose/pump/ProfileGateWizardStep.kt | 0 .../pump/ProfileGateWizardStepPreviews.kt | 0 .../ui/compose/pump/PumpActivityDialog.kt | 0 .../pump/PumpActivityDialogPreviews.kt | 0 .../compose/pump/PumpActivityFabPreviews.kt | 0 .../compose/pump/PumpCommunicationStatus.kt | 0 .../core/ui/compose/pump/PumpHistoryScreen.kt | 0 .../ui/compose/pump/PumpOverviewModels.kt | 0 .../ui/compose/pump/PumpOverviewScreen.kt | 0 .../compose/pump/PumpOverviewStateBuilder.kt | 0 .../aaps/core/ui/compose/pump/WizardScreen.kt | 0 .../siteRotation/ArrowSelectionDialog.kt | 0 .../ArrowSelectionDialogPreviews.kt | 0 .../ui/compose/siteRotation/SiteEntryList.kt | 0 .../siteRotation/SiteEntryListPreviews.kt | 0 .../siteRotation/SiteLocationPicker.kt | 0 .../SiteLocationPickerPreviews.kt | 0 .../siteRotation/SiteLocationPickerScreen.kt | 0 .../SiteLocationPickerScreenPreviews.kt | 0 .../siteRotation/SiteLocationSummary.kt | 0 .../SiteLocationSummaryPreviews.kt | 0 .../siteRotation/SiteLocationWizardStep.kt | 0 .../SiteLocationWizardStepPreviews.kt | 0 .../core/ui/extensions/CobInfoDisplay.kt} | 11 +- .../core/ui/extensions/ContextExtension.kt | 0 .../ui/extensions/TemporaryTargetDisplay.kt | 29 +++ .../aaps/core/ui/extensions/TrendArrowIcon.kt | 33 +++ .../app/aaps/core/ui/extensions/UIUtils.kt | 0 .../app/aaps/core/ui/locale/LocaleHelper.kt | 0 .../app/aaps/core/ui/search/SearchableItem.kt | 0 .../aaps/core/ui/search/SearchableProvider.kt | 0 .../res/drawable/ic_eopatch2_128.xml | 0 .../res/drawable/ic_equil_128.png | Bin .../res/drawable/ic_error_red_48dp.xml | 0 .../res/drawable/ic_medtrum_128.xml | 0 .../res/drawable/ic_notif_aaps.xml | 0 .../res/drawable/notif_icon.png | Bin .../res/drawable/splash_logo.xml | 0 .../res/mipmap-hdpi/ic_blueowl.png | Bin .../res/mipmap-hdpi/ic_greenowl.png | Bin .../res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../res/mipmap-hdpi/ic_pumpcontrol.png | Bin .../res/mipmap-hdpi/ic_yellowowl.png | Bin .../res/mipmap-mdpi/ic_blueowl.png | Bin .../res/mipmap-mdpi/ic_greenowl.png | Bin .../res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../res/mipmap-mdpi/ic_pumpcontrol.png | Bin .../res/mipmap-mdpi/ic_yellowowl.png | Bin .../res/mipmap-xhdpi/ic_blueowl.png | Bin .../res/mipmap-xhdpi/ic_greenowl.png | Bin .../res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../res/mipmap-xhdpi/ic_pumpcontrol.png | Bin .../res/mipmap-xhdpi/ic_yellowowl.png | Bin .../res/mipmap-xxhdpi/ic_blueowl.png | Bin .../res/mipmap-xxhdpi/ic_greenowl.png | Bin .../res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../res/mipmap-xxhdpi/ic_pumpcontrol.png | Bin .../res/mipmap-xxhdpi/ic_yellowowl.png | Bin .../res/mipmap-xxxhdpi/ic_blueowl.png | Bin .../res/mipmap-xxxhdpi/ic_greenowl.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../res/mipmap-xxxhdpi/ic_pumpcontrol.png | Bin .../res/mipmap-xxxhdpi/ic_yellowowl.png | Bin .../{main => androidMain}/res/raw/alarm.mp3 | Bin .../res/raw/boluserror.mp3 | Bin .../{main => androidMain}/res/raw/error.mp3 | Bin .../res/raw/urgentalarm.mp3 | Bin .../res/values-ar-rSA/protection.xml | 0 .../res/values-ar-rSA/strings.xml | 0 .../values-ar-rSA/strings_scene_wizard.xml | 0 .../res/values-bg-rBG/protection.xml | 0 .../res/values-bg-rBG/strings.xml | 0 .../values-bg-rBG/strings_scene_wizard.xml | 0 .../res/values-ca-rES/protection.xml | 0 .../res/values-ca-rES/strings.xml | 0 .../values-ca-rES/strings_scene_wizard.xml | 0 .../res/values-cs-rCZ/protection.xml | 0 .../res/values-cs-rCZ/strings.xml | 0 .../values-cs-rCZ/strings_scene_wizard.xml | 0 .../res/values-cy-rGB/protection.xml | 0 .../res/values-da-rDK/protection.xml | 0 .../res/values-da-rDK/strings.xml | 0 .../values-da-rDK/strings_scene_wizard.xml | 0 .../res/values-de-rDE/protection.xml | 0 .../res/values-de-rDE/strings.xml | 0 .../values-de-rDE/strings_scene_wizard.xml | 0 .../res/values-el-rGR/protection.xml | 0 .../res/values-el-rGR/strings.xml | 0 .../values-el-rGR/strings_scene_wizard.xml | 0 .../res/values-es-rES/protection.xml | 0 .../res/values-es-rES/strings.xml | 0 .../values-es-rES/strings_scene_wizard.xml | 0 .../res/values-fi-rFI/protection.xml | 0 .../res/values-fr-rFR/protection.xml | 0 .../res/values-fr-rFR/strings.xml | 0 .../values-fr-rFR/strings_scene_wizard.xml | 0 .../res/values-hr-rHR/protection.xml | 0 .../res/values-hr-rHR/strings.xml | 0 .../values-hr-rHR/strings_scene_wizard.xml | 0 .../res/values-hu-rHU/protection.xml | 0 .../res/values-hu-rHU/strings.xml | 0 .../values-hu-rHU/strings_scene_wizard.xml | 0 .../res/values-it-rIT/protection.xml | 0 .../res/values-it-rIT/strings.xml | 0 .../values-it-rIT/strings_scene_wizard.xml | 0 .../res/values-iw-rIL/protection.xml | 0 .../res/values-iw-rIL/strings.xml | 0 .../values-iw-rIL/strings_scene_wizard.xml | 0 .../res/values-ko-rKR/protection.xml | 0 .../res/values-ko-rKR/strings.xml | 0 .../values-ko-rKR/strings_scene_wizard.xml | 0 .../res/values-lt-rLT/protection.xml | 0 .../res/values-lt-rLT/strings.xml | 0 .../values-lt-rLT/strings_scene_wizard.xml | 0 .../res/values-nb-rNO/protection.xml | 0 .../res/values-nb-rNO/strings.xml | 0 .../values-nb-rNO/strings_scene_wizard.xml | 0 .../res/values-night/colors.xml | 0 .../res/values-night/styles.xml | 0 .../res/values-nl-rNL/protection.xml | 0 .../res/values-nl-rNL/strings.xml | 0 .../values-nl-rNL/strings_scene_wizard.xml | 0 .../res/values-pl-rPL/protection.xml | 0 .../res/values-pl-rPL/strings.xml | 0 .../values-pl-rPL/strings_scene_wizard.xml | 0 .../res/values-pt-rBR/protection.xml | 0 .../res/values-pt-rBR/strings.xml | 0 .../values-pt-rBR/strings_scene_wizard.xml | 0 .../res/values-pt-rPT/protection.xml | 0 .../res/values-pt-rPT/strings.xml | 0 .../values-pt-rPT/strings_scene_wizard.xml | 0 .../res/values-ro-rRO/protection.xml | 0 .../res/values-ro-rRO/strings.xml | 0 .../values-ro-rRO/strings_scene_wizard.xml | 0 .../res/values-ru-rRU/protection.xml | 0 .../res/values-ru-rRU/strings.xml | 0 .../values-ru-rRU/strings_scene_wizard.xml | 0 .../res/values-sk-rSK/protection.xml | 0 .../res/values-sk-rSK/strings.xml | 0 .../values-sk-rSK/strings_scene_wizard.xml | 0 .../res/values-sl-rSI/protection.xml | 0 .../res/values-sr-rCS/protection.xml | 0 .../res/values-sr-rCS/strings.xml | 0 .../values-sr-rCS/strings_scene_wizard.xml | 0 .../res/values-sv-rSE/protection.xml | 0 .../res/values-sv-rSE/strings.xml | 0 .../values-sv-rSE/strings_scene_wizard.xml | 0 .../res/values-sw600dp/layout.xml | 0 .../res/values-tr-rTR/protection.xml | 0 .../res/values-tr-rTR/strings.xml | 0 .../values-tr-rTR/strings_scene_wizard.xml | 0 .../res/values-uk-rUA/protection.xml | 0 .../res/values-uk-rUA/strings.xml | 0 .../values-uk-rUA/strings_scene_wizard.xml | 0 .../res/values-vi-rVN/protection.xml | 0 .../res/values-vi-rVN/strings.xml | 0 .../values-vi-rVN/strings_scene_wizard.xml | 0 .../res/values-zh-rCN/protection.xml | 0 .../res/values-zh-rCN/strings.xml | 0 .../values-zh-rCN/strings_scene_wizard.xml | 0 .../res/values-zh-rTW/protection.xml | 0 .../res/values-zh-rTW/strings.xml | 0 .../values-zh-rTW/strings_scene_wizard.xml | 0 .../res/values/colors.xml | 0 .../res/values/layout.xml | 0 .../res/values/protection.xml | 0 .../res/values/strings.xml | 0 .../res/values/strings_scene_wizard.xml | 0 .../res/values/styles.xml | 0 .../kotlin/app/aaps/core/ui/UiMode.kt | 0 .../app/aaps/core/ui/compose/AapsCard.kt | 0 .../app/aaps/core/ui/compose/AapsFab.kt | 0 .../app/aaps/core/ui/compose/AapsSpacing.kt | 0 .../app/aaps/core/ui/compose/AapsTopAppBar.kt | 0 .../ui/compose/ComposablePluginContent.kt | 0 .../core/ui/compose/ComposeScreenContent.kt | 0 .../app/aaps/core/ui/compose/GeneralColors.kt | 0 .../app/aaps/core/ui/compose/Modifiers.kt | 0 .../core/ui/compose/ProfileHelperColors.kt | 0 .../aaps/core/ui/compose/QuickAddButtons.kt | 0 .../app/aaps/core/ui/compose/ScreenMode.kt | 0 .../aaps/core/ui/compose/SnackbarColors.kt | 0 .../app/aaps/core/ui/compose/StateColors.kt | 0 .../app/aaps/core/ui/compose/TonalIcon.kt | 0 .../app/aaps/core/ui/compose/ToolbarConfig.kt | 0 .../app/aaps/core/ui/compose/banner/Banner.kt | 0 .../aaps/core/ui/compose/icons/Careportal.kt | 0 .../app/aaps/core/ui/compose/icons/IcAaps.kt | 0 .../aaps/core/ui/compose/icons/IcAction.kt | 0 .../aaps/core/ui/compose/icons/IcActivity.kt | 0 .../core/ui/compose/icons/IcAnnouncement.kt | 0 .../ui/compose/icons/IcArrowDoubleDown.kt | 0 .../core/ui/compose/icons/IcArrowDoubleUp.kt | 0 .../aaps/core/ui/compose/icons/IcArrowFlat.kt | 0 .../ui/compose/icons/IcArrowFortyFiveDown.kt | 0 .../ui/compose/icons/IcArrowFortyFiveUp.kt | 0 .../core/ui/compose/icons/IcArrowInvalid.kt | 0 .../aaps/core/ui/compose/icons/IcArrowLeft.kt | 0 .../core/ui/compose/icons/IcArrowLeftDown.kt | 0 .../core/ui/compose/icons/IcArrowLeftUp.kt | 0 .../aaps/core/ui/compose/icons/IcArrowNone.kt | 0 .../ui/compose/icons/IcArrowSimpleDown.kt | 0 .../core/ui/compose/icons/IcArrowSimpleUp.kt | 0 .../app/aaps/core/ui/compose/icons/IcAs.kt | 0 .../aaps/core/ui/compose/icons/IcAsAbove.kt | 0 .../aaps/core/ui/compose/icons/IcAsAboveX.kt | 0 .../aaps/core/ui/compose/icons/IcAsBelow.kt | 0 .../aaps/core/ui/compose/icons/IcAsBelowX.kt | 0 .../app/aaps/core/ui/compose/icons/IcAsX.kt | 0 .../core/ui/compose/icons/IcAutomation.kt | 0 .../aaps/core/ui/compose/icons/IcBgCheck.kt | 0 .../app/aaps/core/ui/compose/icons/IcBolus.kt | 0 .../app/aaps/core/ui/compose/icons/IcBread.kt | 0 .../app/aaps/core/ui/compose/icons/IcByoda.kt | 0 .../app/aaps/core/ui/compose/icons/IcCake.kt | 0 .../core/ui/compose/icons/IcCalculator.kt | 0 .../core/ui/compose/icons/IcCalibration.kt | 0 .../ui/compose/icons/IcCancelExtendedBolus.kt | 0 .../core/ui/compose/icons/IcCannulaChange.kt | 0 .../app/aaps/core/ui/compose/icons/IcCarbs.kt | 0 .../aaps/core/ui/compose/icons/IcCgmInsert.kt | 0 .../core/ui/compose/icons/IcClinicalNotes.kt | 0 .../ui/compose/icons/IcCompareProfiles.kt | 0 .../app/aaps/core/ui/compose/icons/IcDelta.kt | 0 .../aaps/core/ui/compose/icons/IcDiaconn.kt | 0 .../core/ui/compose/icons/IcExtendedBolus.kt | 0 .../core/ui/compose/icons/IcGenericCgm.kt | 0 .../core/ui/compose/icons/IcGenericIcon.kt | 0 .../core/ui/compose/icons/IcGoogleDrive.kt | 0 .../aaps/core/ui/compose/icons/IcHistory.kt | 0 .../core/ui/compose/icons/IcLoopClosed.kt | 0 .../core/ui/compose/icons/IcLoopDisabled.kt | 0 .../ui/compose/icons/IcLoopDisconnected.kt | 0 .../core/ui/compose/icons/IcLoopHidden.kt | 0 .../aaps/core/ui/compose/icons/IcLoopLgs.kt | 0 .../aaps/core/ui/compose/icons/IcLoopOpen.kt | 0 .../core/ui/compose/icons/IcLoopPaused.kt | 0 .../core/ui/compose/icons/IcLoopPausedDst.kt | 0 .../core/ui/compose/icons/IcLoopPausedPump.kt | 0 .../core/ui/compose/icons/IcLoopReconnect.kt | 0 .../core/ui/compose/icons/IcLoopSuperBolus.kt | 0 .../app/aaps/core/ui/compose/icons/IcMdi.kt | 0 .../app/aaps/core/ui/compose/icons/IcNoTbr.kt | 0 .../app/aaps/core/ui/compose/icons/IcNote.kt | 0 .../aaps/core/ui/compose/icons/IcPatchPump.kt | 0 .../app/aaps/core/ui/compose/icons/IcPizza.kt | 0 .../ui/compose/icons/IcPluginAutomation.kt | 0 .../core/ui/compose/icons/IcPluginAutotune.kt | 0 .../core/ui/compose/icons/IcPluginByoda.kt | 0 .../core/ui/compose/icons/IcPluginCombo.kt | 0 .../ui/compose/icons/IcPluginConfigBuilder.kt | 0 .../core/ui/compose/icons/IcPluginDana.kt | 0 .../core/ui/compose/icons/IcPluginDiaconn.kt | 0 .../core/ui/compose/icons/IcPluginEopatch.kt | 0 .../core/ui/compose/icons/IcPluginEquil.kt | 0 .../ui/compose/icons/IcPluginEversense.kt | 0 .../core/ui/compose/icons/IcPluginFood.kt | 0 .../core/ui/compose/icons/IcPluginGarmin.kt | 0 .../core/ui/compose/icons/IcPluginGlimp.kt | 0 .../core/ui/compose/icons/IcPluginGlunovo.kt | 0 .../core/ui/compose/icons/IcPluginInsight.kt | 0 .../core/ui/compose/icons/IcPluginInsulin.kt | 0 .../ui/compose/icons/IcPluginIntelligo.kt | 0 .../ui/compose/icons/IcPluginMaintenance.kt | 0 .../ui/compose/icons/IcPluginMedtronic.kt | 0 .../core/ui/compose/icons/IcPluginMedtrum.kt | 0 .../core/ui/compose/icons/IcPluginMm640G.kt | 0 .../core/ui/compose/icons/IcPluginNsClient.kt | 0 .../ui/compose/icons/IcPluginNsClientBg.kt | 0 .../ui/compose/icons/IcPluginObjectives.kt | 0 .../core/ui/compose/icons/IcPluginOmnipod.kt | 0 .../core/ui/compose/icons/IcPluginOpenAps.kt | 0 .../ui/compose/icons/IcPluginOpenHumans.kt | 0 .../core/ui/compose/icons/IcPluginPocTech.kt | 0 .../core/ui/compose/icons/IcPluginRandomBg.kt | 0 .../aaps/core/ui/compose/icons/IcPluginSms.kt | 0 .../core/ui/compose/icons/IcPluginSyai.kt | 0 .../core/ui/compose/icons/IcPluginTMobi.kt | 0 .../core/ui/compose/icons/IcPluginTidepool.kt | 0 .../core/ui/compose/icons/IcPluginTizen.kt | 0 .../core/ui/compose/icons/IcPluginTomato.kt | 0 .../ui/compose/icons/IcPluginVirtualPump.kt | 0 .../aaps/core/ui/compose/icons/IcProfile.kt | 0 .../core/ui/compose/icons/IcPumpBattery.kt | 0 .../core/ui/compose/icons/IcPumpCartridge.kt | 0 .../aaps/core/ui/compose/icons/IcQuestion.kt | 0 .../core/ui/compose/icons/IcQuickWizard.kt | 0 .../core/ui/compose/icons/IcSettingsOff.kt | 0 .../core/ui/compose/icons/IcSetupWizard.kt | 0 .../core/ui/compose/icons/IcSiteRotation.kt | 0 .../app/aaps/core/ui/compose/icons/IcSmb.kt | 0 .../app/aaps/core/ui/compose/icons/IcStats.kt | 0 .../aaps/core/ui/compose/icons/IcTbrCancel.kt | 0 .../aaps/core/ui/compose/icons/IcTbrHigh.kt | 0 .../aaps/core/ui/compose/icons/IcTbrLow.kt | 0 .../core/ui/compose/icons/IcTtActivity.kt | 0 .../aaps/core/ui/compose/icons/IcTtCancel.kt | 0 .../core/ui/compose/icons/IcTtEatingSoon.kt | 0 .../aaps/core/ui/compose/icons/IcTtHigh.kt | 0 .../aaps/core/ui/compose/icons/IcTtHypo.kt | 0 .../aaps/core/ui/compose/icons/IcTtManual.kt | 0 .../core/ui/compose/icons/IcUserOptions.kt | 0 .../app/aaps/core/ui/compose/icons/IcXDrip.kt | 0 .../app/aaps/core/ui/compose/icons/Ns.kt | 0 .../app/aaps/core/ui/compose/icons/Pump.kt | 0 .../ui/compose/icons/library/IcChildBack.kt | 0 .../ui/compose/icons/library/IcChildFront.kt | 0 .../ui/compose/icons/library/IcManBack.kt | 0 .../ui/compose/icons/library/IcManFront.kt | 0 .../ui/compose/icons/library/IcWomanBack.kt | 0 .../ui/compose/icons/library/IcWomanFront.kt | 0 .../library/unused/IcActivityTreatments.kt | 0 .../icons/library/unused/IcArrowCenter.kt | 0 .../icons/library/unused/IcArrowFlat.kt | 0 .../icons/library/unused/IcPluginAction.kt | 0 .../library/unused/IcPluginConfigBuilder.kt | 0 .../icons/library/unused/IcPluginOverview.kt | 0 .../ui/compose/navigation/ElementColors.kt | 0 .../compose/navigation/NavigationRequest.kt | 0 .../ui/compose/preference/BasicPreference.kt | 0 .../compose/preference/LocalPasswordCheck.kt | 0 .../preference/PaddingValuesExtensions.kt | 0 .../preference/PreferenceScreenContent.kt | 0 .../preference/PreferenceSubScreenDef.kt | 0 .../ui/compose/preference/ScrollIndicators.kt | 0 .../core/ui/compose/pump/PumpActivityFab.kt | 0 .../core/ui/compose/pump/PumpHistoryModels.kt | 0 .../ui/compose/pump/StepProgressIndicator.kt | 0 .../aaps/core/ui/compose/pump/TickerFlow.kt | 0 .../core/ui/compose/pump/WizardStepLayout.kt | 0 .../compose/siteRotation/ArrowExtensions.kt | 0 .../core/ui/compose/siteRotation/BodyType.kt | 0 .../core/ui/compose/siteRotation/BodyView.kt | 0 .../siteRotation/ZoomableBodyDiagram.kt | 0 .../app/aaps/core/ui/elements/WeekDay.kt | 0 .../actions/ActionStartTempTarget.kt | 2 +- .../PersistentNotificationPlugin.kt | 2 +- .../extensions/GlucoseValueExtensionKtTest.kt | 2 +- .../TemporaryTargetExtensionKtTest.kt | 4 +- .../plugins/source/compose/BgSourceScreen.kt | 2 +- .../smsCommunicator/SmsCommunicatorPlugin.kt | 2 +- .../wear/wearintegration/DataHandlerMobile.kt | 2 +- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 2 +- .../compose/overview/chips/ChipsViewModel.kt | 2 +- .../ui/compose/treatments/TempTargetScreen.kt | 4 +- .../viewmodels/TempTargetViewModel.kt | 2 +- .../ui/widget/glance/WidgetStateLoader.kt | 2 +- 625 files changed, 225 insertions(+), 114 deletions(-) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/AapsCardTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/AapsFabTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/AapsSearchFieldTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/QuickAddButtonsTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/TonalIconTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItemsTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/preference/PreferenceComponentsTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContentTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt (100%) rename core/ui/src/{test => androidHostTest}/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt (100%) rename core/ui/src/{main => androidMain}/AndroidManifest.xml (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/AlarmSoundResources.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsTheme.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/AapsTypography.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/FormatUtils.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/StatusLevel.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/TextRefResource.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/Preference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/BluetoothPermissionsHost.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/KeepScreenOnEffect.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt (100%) rename core/{objects/src/main/kotlin/app/aaps/core/objects/extensions/CobInfoExtension.kt => ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt} (67%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/extensions/ContextExtension.kt (100%) create mode 100644 core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt create mode 100644 core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/extensions/UIUtils.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/search/SearchableItem.kt (100%) rename core/ui/src/{main => androidMain}/kotlin/app/aaps/core/ui/search/SearchableProvider.kt (100%) rename core/ui/src/{main => androidMain}/res/drawable/ic_eopatch2_128.xml (100%) rename core/ui/src/{main => androidMain}/res/drawable/ic_equil_128.png (100%) rename core/ui/src/{main => androidMain}/res/drawable/ic_error_red_48dp.xml (100%) rename core/ui/src/{main => androidMain}/res/drawable/ic_medtrum_128.xml (100%) rename core/ui/src/{main => androidMain}/res/drawable/ic_notif_aaps.xml (100%) rename core/ui/src/{main => androidMain}/res/drawable/notif_icon.png (100%) rename core/ui/src/{main => androidMain}/res/drawable/splash_logo.xml (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_blueowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_greenowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_launcher.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_launcher_round.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_pumpcontrol.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-hdpi/ic_yellowowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_blueowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_greenowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_launcher.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_launcher_round.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_pumpcontrol.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-mdpi/ic_yellowowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_blueowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_greenowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_launcher.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_pumpcontrol.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xhdpi/ic_yellowowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_blueowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_greenowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_launcher.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_pumpcontrol.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxhdpi/ic_yellowowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_blueowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_greenowl.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_pumpcontrol.png (100%) rename core/ui/src/{main => androidMain}/res/mipmap-xxxhdpi/ic_yellowowl.png (100%) rename core/ui/src/{main => androidMain}/res/raw/alarm.mp3 (100%) rename core/ui/src/{main => androidMain}/res/raw/boluserror.mp3 (100%) rename core/ui/src/{main => androidMain}/res/raw/error.mp3 (100%) rename core/ui/src/{main => androidMain}/res/raw/urgentalarm.mp3 (100%) rename core/ui/src/{main => androidMain}/res/values-ar-rSA/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ar-rSA/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ar-rSA/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-bg-rBG/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-bg-rBG/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-bg-rBG/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ca-rES/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ca-rES/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ca-rES/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-cs-rCZ/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-cs-rCZ/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-cs-rCZ/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-cy-rGB/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-da-rDK/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-da-rDK/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-da-rDK/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-de-rDE/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-de-rDE/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-de-rDE/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-el-rGR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-el-rGR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-el-rGR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-es-rES/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-es-rES/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-es-rES/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-fi-rFI/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-fr-rFR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-fr-rFR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-fr-rFR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hr-rHR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hr-rHR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hr-rHR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hu-rHU/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hu-rHU/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-hu-rHU/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-it-rIT/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-it-rIT/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-it-rIT/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-iw-rIL/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-iw-rIL/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-iw-rIL/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ko-rKR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ko-rKR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ko-rKR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-lt-rLT/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-lt-rLT/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-lt-rLT/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nb-rNO/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nb-rNO/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nb-rNO/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-night/colors.xml (100%) rename core/ui/src/{main => androidMain}/res/values-night/styles.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nl-rNL/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nl-rNL/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-nl-rNL/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pl-rPL/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pl-rPL/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pl-rPL/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rBR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rBR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rBR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rPT/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rPT/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-pt-rPT/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ro-rRO/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ro-rRO/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ro-rRO/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ru-rRU/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ru-rRU/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-ru-rRU/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sk-rSK/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sk-rSK/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sk-rSK/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sl-rSI/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sr-rCS/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sr-rCS/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sr-rCS/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sv-rSE/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sv-rSE/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sv-rSE/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-sw600dp/layout.xml (100%) rename core/ui/src/{main => androidMain}/res/values-tr-rTR/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-tr-rTR/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-tr-rTR/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-uk-rUA/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-uk-rUA/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-uk-rUA/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-vi-rVN/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-vi-rVN/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-vi-rVN/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rCN/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rCN/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rCN/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rTW/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rTW/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values-zh-rTW/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values/colors.xml (100%) rename core/ui/src/{main => androidMain}/res/values/layout.xml (100%) rename core/ui/src/{main => androidMain}/res/values/protection.xml (100%) rename core/ui/src/{main => androidMain}/res/values/strings.xml (100%) rename core/ui/src/{main => androidMain}/res/values/strings_scene_wizard.xml (100%) rename core/ui/src/{main => androidMain}/res/values/styles.xml (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/UiMode.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/AapsCard.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/AapsFab.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/AapsSpacing.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/AapsTopAppBar.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/ComposablePluginContent.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/ComposeScreenContent.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/GeneralColors.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/Modifiers.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/ProfileHelperColors.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/QuickAddButtons.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/ScreenMode.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/SnackbarColors.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/StateColors.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/TonalIcon.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/ToolbarConfig.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/banner/Banner.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/Careportal.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAaps.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAction.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcActivity.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncement.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDown.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUp.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlat.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDown.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUp.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalid.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeft.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDown.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUp.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowNone.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDown.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUp.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAs.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAbove.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveX.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelow.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowX.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsX.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAutomation.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBgCheck.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBolus.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBread.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcByoda.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCake.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalculator.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalibration.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolus.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChange.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCarbs.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsert.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotes.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfiles.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcDelta.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcDiaconn.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolus.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgm.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericIcon.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrive.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcHistory.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosed.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabled.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnected.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopHidden.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgs.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpen.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPaused.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDst.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPump.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnect.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolus.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcMdi.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcNoTbr.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcNote.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPatchPump.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPizza.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomation.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotune.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginByoda.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginCombo.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilder.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDana.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconn.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatch.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquil.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversense.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginFood.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarmin.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimp.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovo.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsight.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulin.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligo.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenance.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronic.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrum.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640G.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClient.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBg.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectives.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipod.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenAps.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumans.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTech.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBg.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSms.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyai.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobi.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepool.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizen.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomato.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPump.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcProfile.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpBattery.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridge.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuestion.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizard.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOff.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizard.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotation.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSmb.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcStats.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancel.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrHigh.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrLow.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtActivity.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtCancel.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoon.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHigh.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHypo.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtManual.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcUserOptions.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcXDrip.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/Ns.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/Pump.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatments.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenter.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlat.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginAction.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilder.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverview.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/navigation/ElementColors.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/navigation/NavigationRequest.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/BasicPreference.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/LocalPasswordCheck.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PaddingValuesExtensions.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/preference/ScrollIndicators.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFab.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryModels.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/pump/StepProgressIndicator.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/pump/TickerFlow.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/pump/WizardStepLayout.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowExtensions.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ZoomableBodyDiagram.kt (100%) rename core/ui/src/{main => commonMain}/kotlin/app/aaps/core/ui/elements/WeekDay.kt (100%) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/GlucoseValueExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/GlucoseValueExtension.kt index f45a59962664..5abd6b0581a7 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/GlucoseValueExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/GlucoseValueExtension.kt @@ -1,20 +1,10 @@ package app.aaps.core.objects.extensions -import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.data.configuration.Constants import app.aaps.core.data.iob.InMemoryGlucoseValue import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit -import app.aaps.core.data.model.TrendArrow import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.ui.compose.icons.IcArrowDoubleDown -import app.aaps.core.ui.compose.icons.IcArrowDoubleUp -import app.aaps.core.ui.compose.icons.IcArrowFlat -import app.aaps.core.ui.compose.icons.IcArrowFortyfiveDown -import app.aaps.core.ui.compose.icons.IcArrowFortyfiveUp -import app.aaps.core.ui.compose.icons.IcArrowInvalid -import app.aaps.core.ui.compose.icons.IcArrowSimpleDown -import app.aaps.core.ui.compose.icons.IcArrowSimpleUp import org.json.JSONObject fun GV.toJson(isAdd: Boolean, dateUtil: DateUtil): JSONObject = @@ -31,17 +21,3 @@ fun GV.toJson(isAdd: Boolean, dateUtil: DateUtil): JSONObject = fun InMemoryGlucoseValue.valueToUnits(units: GlucoseUnit): Double = if (units == GlucoseUnit.MGDL) recalculated else recalculated * Constants.MGDL_TO_MMOLL - -fun TrendArrow.directionToIcon(): ImageVector = - when (this) { - TrendArrow.TRIPLE_DOWN -> IcArrowInvalid - TrendArrow.DOUBLE_DOWN -> IcArrowDoubleDown - TrendArrow.SINGLE_DOWN -> IcArrowSimpleDown - TrendArrow.FORTY_FIVE_DOWN -> IcArrowFortyfiveDown - TrendArrow.FLAT -> IcArrowFlat - TrendArrow.FORTY_FIVE_UP -> IcArrowFortyfiveUp - TrendArrow.SINGLE_UP -> IcArrowSimpleUp - TrendArrow.DOUBLE_UP -> IcArrowDoubleUp - TrendArrow.TRIPLE_UP -> IcArrowInvalid - TrendArrow.NONE -> IcArrowInvalid - } diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryTargetExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryTargetExtension.kt index 80f45a489aba..c9b3bf4597b0 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryTargetExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryTargetExtension.kt @@ -1,25 +1,6 @@ package app.aaps.core.objects.extensions -import app.aaps.core.data.configuration.Constants -import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TT -import app.aaps.core.interfaces.profile.ProfileUtil -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DecimalFormatter -import kotlin.time.Duration.Companion.milliseconds - -fun TT.lowValueToUnitsToString(units: GlucoseUnit, decimalFormatter: DecimalFormatter): String = - if (units == GlucoseUnit.MGDL) decimalFormatter.to0Decimal(this.lowTarget) - else decimalFormatter.to1Decimal(this.lowTarget * Constants.MGDL_TO_MMOLL) - -fun TT.highValueToUnitsToString(units: GlucoseUnit, decimalFormatter: DecimalFormatter): String = - if (units == GlucoseUnit.MGDL) decimalFormatter.to0Decimal(this.highTarget) - else decimalFormatter.to1Decimal(this.highTarget * Constants.MGDL_TO_MMOLL) fun TT.target(): Double = (this.lowTarget + this.highTarget) / 2 - -fun TT.friendlyDescription(units: GlucoseUnit, rh: ResourceHelper, profileUtil: ProfileUtil): String = - profileUtil.toTargetRangeString(lowTarget, highTarget, GlucoseUnit.MGDL, units) + - profileUtil.unitLabel + - "@" + rh.gs(app.aaps.core.ui.R.string.format_mins, duration.milliseconds.inWholeMinutes) + "(" + reason.text + ")" diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index b10dbcd194ad..a7de9de896ea 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,70 +1,155 @@ -import com.android.build.api.variant.LibraryAndroidComponentsExtension import kotlin.math.min +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension plugins { - alias(libs.plugins.android.library) + kotlin("multiplatform") + // NOT com.android.library. AGP 9 refuses that plugin together with the multiplatform plugin. + // Same reason as :core:keys, :core:data, :core:utils and :core:interfaces. + alias(libs.plugins.android.kmp.library) + // The Compose COMPILER, which ships with Kotlin and compiles @Composable for every target. alias(libs.plugins.compose.compiler) - id("android-module-dependencies") - id("test-module-dependencies") - id("compose-test-module-dependencies") - id("jacoco-module-dependencies") + // The Compose Multiplatform framework. The compiler plugin above is applied per project rather + // than per target, so every target needs a Compose runtime on its class path. + alias(libs.plugins.compose.multiplatform) } -android { - namespace = "app.aaps.core.ui" - defaultConfig { - minSdk = min(Versions.minSdk, Versions.wearMinSdk) - } - - buildFeatures { - compose = true - } +// One task, not one per variant: a multiplatform module has no product flavours, and a Kotlin source +// set takes a task provider directly. Same generator as :core:keys and :core:interfaces, pointed at +// this module's strings. The strings themselves do not move, and AAPT keeps resolving them on +// Android exactly as before. +val generateUiStrings = tasks.register("generateUiStrings") { + resDir.set(layout.projectDirectory.dir("src/androidMain/res")) + packageName.set("app.aaps.core.ui") + owner.set("ui") + objectName.set("UiStrings") + idsObjectName.set("UiStringIds") + reportFile.set(layout.buildDirectory.file("reports/uiStrings/translations.txt")) + // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the task + // name, so both properties would land on one directory and the second file written would delete + // the first. + commonOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/common")) + androidOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/android")) } -// Same generator as :core:keys, pointed at this module's strings. It removes R.string from the -// Compose call sites so they stop being Android-only; the strings themselves do not move, and AAPT -// keeps resolving them on Android exactly as before. -extensions.configure("androidComponents") { - onVariants { variant -> - val taskProvider = tasks.register( - "generate${variant.name.replaceFirstChar { it.uppercase() }}UiStrings", - GenerateKeyStringsTask::class.java - ) { - resDir.set(layout.projectDirectory.dir("src/main/res")) - packageName.set("app.aaps.core.ui") - owner.set("ui") - objectName.set("UiStrings") - idsObjectName.set("UiStringIds") - reportFile.set(layout.buildDirectory.file("reports/uiStrings/${variant.name}-translations.txt")) - // Set explicitly: addGeneratedSourceDirectory only applies a convention derived from the - // task name, so both properties would land on one directory and the second file written - // would delete the first. - commonOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/${variant.name}/common")) - androidOutputDir.set(layout.buildDirectory.dir("generated/uiStrings/${variant.name}/android")) +kotlin { + android { + namespace = "app.aaps.core.ui" + compileSdk = Versions.compileSdk + minSdk = min(Versions.minSdk, Versions.wearMinSdk) // Compatible with wear module + // Off by default for a multiplatform library, unlike a plain android library. This module is + // where most of the app's strings, drawables and raw alarm sounds live, so it must be on. + androidResources { enable = true } + // Creates the androidHostTest compilation, which also pulls in commonTest. + // isIncludeAndroidResources is what makes Robolectric work: the Compose UI tests here need a + // real merged resource table and the manifest that holds the activity createComposeRule() + // launches. Restated from test-module-dependencies, which this module can no longer apply. + withHostTest { + isIncludeAndroidResources = true + isReturnDefaultValues = true + } + compilerOptions { jvmTarget.set(Versions.jvmTarget) } + + // Restated from android-module-dependencies, which this module can no longer apply. Without + // it MissingTranslation would switch on for the first time here and every locale file that + // is incomplete today would fail a release build. + lint { + checkReleaseBuilds = false + disable += "MissingTranslation" + disable += "ExtraTranslation" } - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::commonOutputDir) - variant.sources.kotlin?.addGeneratedSourceDirectory(taskProvider, GenerateKeyStringsTask::androidOutputDir) } -} -dependencies { - api(libs.androidx.appcompat) + // Apple klibs cross compile on Windows. Linking and running still need a Mac, and those tasks + // report SKIPPED rather than failing. Keeping the target is what stops an android-only import + // from quietly reaching commonMain later. + // + // Deliberately no jvm() target, as in :core:interfaces: it pulls in the desktop Compose surface + // (skiko-awt) and gives the module another way to fail without saying anything about iOS. + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + kotlin.srcDir(generateUiStrings.flatMap { it.commonOutputDir }) + dependencies { + api(project(":core:data")) + api(project(":core:interfaces")) + api(project(":core:keys")) + api(libs.kotlinx.datetime) + + // CMP rather than androidx. On Android CMP delegates to androidx, so the composeBom + // below still decides the Android versions and nothing about the Android build + // changes. + api(libs.cmp.runtime) + api(libs.cmp.foundation) + api(libs.cmp.ui) + api(libs.cmp.material3) + api(libs.cmp.material.icons.extended) + } + } + + androidMain { + // Android only: the string name to R.string id map. + kotlin.srcDir(generateUiStrings.flatMap { it.androidOutputDir }) + dependencies { + // Everything here was `api` on the old android library and the consumer modules + // resolve it transitively, so it must stay exported. + api(libs.androidx.appcompat) + api(libs.com.google.android.material) + api(project.dependencies.platform(libs.androidx.compose.bom)) + api(libs.androidx.compose.material3) + api(libs.androidx.compose.material.icons.extended) + api(libs.androidx.compose.runtime) + api(libs.androidx.activity.compose) + api(libs.androidx.lifecycle.runtime.compose) - api(libs.com.google.android.material) - api(platform(libs.androidx.compose.bom)) - api(libs.androidx.compose.material3) - api(libs.androidx.compose.material.icons.extended) - api(libs.androidx.compose.runtime) - api(libs.androidx.activity.compose) - api(libs.androidx.lifecycle.runtime.compose) + api(libs.com.google.dagger.android) + api(libs.com.google.dagger.android.support) + + implementation(libs.androidx.compose.ui.tooling.preview) + // Was debugImplementation. The AGP multiplatform library target has no build types, + // so there is no debug-only configuration to put it in. It only matters for rendering + // @Preview, and R8 in the app module drops it from a release build. + implementation(libs.androidx.compose.ui.tooling) + } + } - api(libs.com.google.dagger.android) - api(libs.com.google.dagger.android.support) + // Hand written rather than taken from test-module-dependencies and + // compose-test-module-dependencies, because both convention plugins apply + // com.android.library and so cannot be used here. + // + // createComposeRule() is a JUnit4 TestRule and RobolectricTestRunner is a JUnit4 runner, so + // these tests run JUnit4 style; the vintage engine bridges them onto the JUnit Platform. + getByName("androidHostTest") { + dependencies { + implementation(libs.org.junit.jupiter) + implementation(libs.org.junit.jupiter.api) + implementation(libs.org.mockito.junit.jupiter) + implementation(libs.org.mockito.kotlin) + implementation(libs.com.google.truth) + implementation(libs.kotlinx.coroutines.test) + implementation(project.dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui.test.junit4) + implementation(libs.org.robolectric) + // Was debugImplementation: supplies the manifest holding the activity that + // createComposeRule() launches. + implementation(libs.androidx.compose.ui.test.manifest) + runtimeOnly(libs.org.junit.vintage.engine) + runtimeOnly(libs.org.junit.platform.launcher) + } + } + } +} - implementation(project(":core:interfaces")) - implementation(project(":core:keys")) - implementation(project(":core:data")) - implementation(libs.kotlinx.datetime) - implementation(libs.androidx.compose.ui.tooling.preview) - debugImplementation(libs.androidx.compose.ui.tooling) +tasks.withType { + useJUnitPlatform() + // Robolectric runs tests in its own classloader sandbox and rewrites bytecode, so the default + // JaCoCo on-the-fly agent records no coverage for the classes those tests exercise - here that is + // every Compose screen the UI tests drive. Restated from jacoco-module-dependencies, which applies + // com.android.library and so cannot be used by a multiplatform module. The jacoco plugin itself is + // already applied to every project by the root build file. + extensions.configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*") + } } diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsCardTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsCardTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsCardTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsCardTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsFabTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsFabTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsFabTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsFabTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsSearchFieldTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsSearchFieldTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/AapsSearchFieldTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/AapsSearchFieldTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/QuickAddButtonsTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/QuickAddButtonsTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/QuickAddButtonsTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/QuickAddButtonsTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/TonalIconTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/TonalIconTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/TonalIconTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/TonalIconTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHostTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHostTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyleTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItemsTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItemsTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItemsTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItemsTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/MorePreferenceComponentsTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/PreferenceComponentsTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/PreferenceComponentsTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/PreferenceComponentsTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/PreferenceComponentsTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContentTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContentTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContentTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContentTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreenTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/siteRotation/BodyViewContainsPointTest.kt diff --git a/core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt similarity index 100% rename from core/ui/src/test/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt rename to core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/elements/WeekDayTest.kt diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/androidMain/AndroidManifest.xml similarity index 100% rename from core/ui/src/main/AndroidManifest.xml rename to core/ui/src/androidMain/AndroidManifest.xml diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/AlarmSoundResources.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/AlarmSoundResources.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/AlarmSoundResources.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTheme.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTheme.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTypography.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTypography.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/FormatUtils.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ImportSummaryComponents.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/StatusLevel.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/StatusLevel.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/TextRefResource.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceList.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/Preference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/Preference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceContentExtensions.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSheetContent.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BluetoothPermissionsHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BluetoothPermissionsHost.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/BluetoothPermissionsHost.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BluetoothPermissionsHost.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/KeepScreenOnEffect.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/KeepScreenOnEffect.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/KeepScreenOnEffect.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/KeepScreenOnEffect.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/CobInfoExtension.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt similarity index 67% rename from core/objects/src/main/kotlin/app/aaps/core/objects/extensions/CobInfoExtension.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt index 559c22a9ff38..e07c0b21f09f 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/CobInfoExtension.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt @@ -1,9 +1,16 @@ -package app.aaps.core.objects.extensions +package app.aaps.core.ui.extensions import app.aaps.core.data.iob.CobInfo import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DecimalFormatter +import app.aaps.core.ui.R +/** + * Text for a [CobInfo] on screen. + * + * These build user visible text out of this module's strings, so they belong here. They used to sit + * in `:core:objects`, which made a domain module depend on this one only to name a string. + */ fun CobInfo.generateCOBString(decimalFormatter: DecimalFormatter): String { var cobStringResult = "--g" displayCob?.let { displayCob -> @@ -17,7 +24,7 @@ fun CobInfo.generateCOBString(decimalFormatter: DecimalFormatter): String { fun CobInfo.displayText(rh: ResourceHelper, decimalFormatter: DecimalFormatter): String? = displayCob?.let { displayCob -> - var cobText = rh.gs(app.aaps.core.ui.R.string.format_carbs, displayCob.toInt()) + var cobText = rh.gs(R.string.format_carbs, displayCob.toInt()) if (futureCarbs > 0) cobText += "(" + decimalFormatter.to0Decimal(futureCarbs) + ")" cobText } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/extensions/ContextExtension.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/ContextExtension.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/extensions/ContextExtension.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/ContextExtension.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt new file mode 100644 index 000000000000..e6dda5026c39 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt @@ -0,0 +1,29 @@ +package app.aaps.core.ui.extensions + +import app.aaps.core.data.configuration.Constants +import app.aaps.core.data.model.GlucoseUnit +import app.aaps.core.data.model.TT +import app.aaps.core.interfaces.profile.ProfileUtil +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.utils.DecimalFormatter +import app.aaps.core.ui.R +import kotlin.time.Duration.Companion.milliseconds + +/** + * Text for a [TT] on screen. + * + * The maths that belongs to the model - `TT.target()` - stays in `:core:objects`. Only the part that + * formats for a reader lives here, next to the strings it uses. + */ +fun TT.lowValueToUnitsToString(units: GlucoseUnit, decimalFormatter: DecimalFormatter): String = + if (units == GlucoseUnit.MGDL) decimalFormatter.to0Decimal(this.lowTarget) + else decimalFormatter.to1Decimal(this.lowTarget * Constants.MGDL_TO_MMOLL) + +fun TT.highValueToUnitsToString(units: GlucoseUnit, decimalFormatter: DecimalFormatter): String = + if (units == GlucoseUnit.MGDL) decimalFormatter.to0Decimal(this.highTarget) + else decimalFormatter.to1Decimal(this.highTarget * Constants.MGDL_TO_MMOLL) + +fun TT.friendlyDescription(units: GlucoseUnit, rh: ResourceHelper, profileUtil: ProfileUtil): String = + profileUtil.toTargetRangeString(lowTarget, highTarget, GlucoseUnit.MGDL, units) + + profileUtil.unitLabel + + "@" + rh.gs(R.string.format_mins, duration.milliseconds.inWholeMinutes) + "(" + reason.text + ")" diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt new file mode 100644 index 000000000000..315073e4c08d --- /dev/null +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt @@ -0,0 +1,33 @@ +package app.aaps.core.ui.extensions + +import androidx.compose.ui.graphics.vector.ImageVector +import app.aaps.core.data.model.TrendArrow +import app.aaps.core.ui.compose.icons.IcArrowDoubleDown +import app.aaps.core.ui.compose.icons.IcArrowDoubleUp +import app.aaps.core.ui.compose.icons.IcArrowFlat +import app.aaps.core.ui.compose.icons.IcArrowFortyfiveDown +import app.aaps.core.ui.compose.icons.IcArrowFortyfiveUp +import app.aaps.core.ui.compose.icons.IcArrowInvalid +import app.aaps.core.ui.compose.icons.IcArrowSimpleDown +import app.aaps.core.ui.compose.icons.IcArrowSimpleUp + +/** + * The icon that draws a [TrendArrow]. + * + * Lives here and not next to the model, for the same reason a model does not carry a colour: + * [TrendArrow] is the classification, and picking a picture for it is the job of the layer that + * draws. It used to sit in `:core:objects`, which made a domain module depend on this one. + */ +fun TrendArrow.directionToIcon(): ImageVector = + when (this) { + TrendArrow.TRIPLE_DOWN -> IcArrowInvalid + TrendArrow.DOUBLE_DOWN -> IcArrowDoubleDown + TrendArrow.SINGLE_DOWN -> IcArrowSimpleDown + TrendArrow.FORTY_FIVE_DOWN -> IcArrowFortyfiveDown + TrendArrow.FLAT -> IcArrowFlat + TrendArrow.FORTY_FIVE_UP -> IcArrowFortyfiveUp + TrendArrow.SINGLE_UP -> IcArrowSimpleUp + TrendArrow.DOUBLE_UP -> IcArrowDoubleUp + TrendArrow.TRIPLE_UP -> IcArrowInvalid + TrendArrow.NONE -> IcArrowInvalid + } diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/extensions/UIUtils.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/UIUtils.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/extensions/UIUtils.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/UIUtils.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/locale/LocaleHelper.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableItem.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableProvider.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/search/SearchableProvider.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt diff --git a/core/ui/src/main/res/drawable/ic_eopatch2_128.xml b/core/ui/src/androidMain/res/drawable/ic_eopatch2_128.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_eopatch2_128.xml rename to core/ui/src/androidMain/res/drawable/ic_eopatch2_128.xml diff --git a/core/ui/src/main/res/drawable/ic_equil_128.png b/core/ui/src/androidMain/res/drawable/ic_equil_128.png similarity index 100% rename from core/ui/src/main/res/drawable/ic_equil_128.png rename to core/ui/src/androidMain/res/drawable/ic_equil_128.png diff --git a/core/ui/src/main/res/drawable/ic_error_red_48dp.xml b/core/ui/src/androidMain/res/drawable/ic_error_red_48dp.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_error_red_48dp.xml rename to core/ui/src/androidMain/res/drawable/ic_error_red_48dp.xml diff --git a/core/ui/src/main/res/drawable/ic_medtrum_128.xml b/core/ui/src/androidMain/res/drawable/ic_medtrum_128.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_medtrum_128.xml rename to core/ui/src/androidMain/res/drawable/ic_medtrum_128.xml diff --git a/core/ui/src/main/res/drawable/ic_notif_aaps.xml b/core/ui/src/androidMain/res/drawable/ic_notif_aaps.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_notif_aaps.xml rename to core/ui/src/androidMain/res/drawable/ic_notif_aaps.xml diff --git a/core/ui/src/main/res/drawable/notif_icon.png b/core/ui/src/androidMain/res/drawable/notif_icon.png similarity index 100% rename from core/ui/src/main/res/drawable/notif_icon.png rename to core/ui/src/androidMain/res/drawable/notif_icon.png diff --git a/core/ui/src/main/res/drawable/splash_logo.xml b/core/ui/src/androidMain/res/drawable/splash_logo.xml similarity index 100% rename from core/ui/src/main/res/drawable/splash_logo.xml rename to core/ui/src/androidMain/res/drawable/splash_logo.xml diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_blueowl.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_blueowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_blueowl.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_blueowl.png diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_greenowl.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_greenowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_greenowl.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_greenowl.png diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_launcher.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_launcher.png diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_pumpcontrol.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_pumpcontrol.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_pumpcontrol.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_pumpcontrol.png diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_yellowowl.png b/core/ui/src/androidMain/res/mipmap-hdpi/ic_yellowowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-hdpi/ic_yellowowl.png rename to core/ui/src/androidMain/res/mipmap-hdpi/ic_yellowowl.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_blueowl.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_blueowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_blueowl.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_blueowl.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_greenowl.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_greenowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_greenowl.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_greenowl.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_launcher.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_launcher.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_pumpcontrol.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_pumpcontrol.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_pumpcontrol.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_pumpcontrol.png diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_yellowowl.png b/core/ui/src/androidMain/res/mipmap-mdpi/ic_yellowowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-mdpi/ic_yellowowl.png rename to core/ui/src/androidMain/res/mipmap-mdpi/ic_yellowowl.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_blueowl.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_blueowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_blueowl.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_blueowl.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_greenowl.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_greenowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_greenowl.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_greenowl.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_launcher.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_pumpcontrol.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_pumpcontrol.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_pumpcontrol.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_pumpcontrol.png diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_yellowowl.png b/core/ui/src/androidMain/res/mipmap-xhdpi/ic_yellowowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xhdpi/ic_yellowowl.png rename to core/ui/src/androidMain/res/mipmap-xhdpi/ic_yellowowl.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_blueowl.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_blueowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_blueowl.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_blueowl.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_greenowl.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_greenowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_greenowl.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_greenowl.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_pumpcontrol.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_pumpcontrol.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_pumpcontrol.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_pumpcontrol.png diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_yellowowl.png b/core/ui/src/androidMain/res/mipmap-xxhdpi/ic_yellowowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxhdpi/ic_yellowowl.png rename to core/ui/src/androidMain/res/mipmap-xxhdpi/ic_yellowowl.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_blueowl.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_blueowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_blueowl.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_blueowl.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_greenowl.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_greenowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_greenowl.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_greenowl.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_pumpcontrol.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_pumpcontrol.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_pumpcontrol.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_pumpcontrol.png diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_yellowowl.png b/core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_yellowowl.png similarity index 100% rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_yellowowl.png rename to core/ui/src/androidMain/res/mipmap-xxxhdpi/ic_yellowowl.png diff --git a/core/ui/src/main/res/raw/alarm.mp3 b/core/ui/src/androidMain/res/raw/alarm.mp3 similarity index 100% rename from core/ui/src/main/res/raw/alarm.mp3 rename to core/ui/src/androidMain/res/raw/alarm.mp3 diff --git a/core/ui/src/main/res/raw/boluserror.mp3 b/core/ui/src/androidMain/res/raw/boluserror.mp3 similarity index 100% rename from core/ui/src/main/res/raw/boluserror.mp3 rename to core/ui/src/androidMain/res/raw/boluserror.mp3 diff --git a/core/ui/src/main/res/raw/error.mp3 b/core/ui/src/androidMain/res/raw/error.mp3 similarity index 100% rename from core/ui/src/main/res/raw/error.mp3 rename to core/ui/src/androidMain/res/raw/error.mp3 diff --git a/core/ui/src/main/res/raw/urgentalarm.mp3 b/core/ui/src/androidMain/res/raw/urgentalarm.mp3 similarity index 100% rename from core/ui/src/main/res/raw/urgentalarm.mp3 rename to core/ui/src/androidMain/res/raw/urgentalarm.mp3 diff --git a/core/ui/src/main/res/values-ar-rSA/protection.xml b/core/ui/src/androidMain/res/values-ar-rSA/protection.xml similarity index 100% rename from core/ui/src/main/res/values-ar-rSA/protection.xml rename to core/ui/src/androidMain/res/values-ar-rSA/protection.xml diff --git a/core/ui/src/main/res/values-ar-rSA/strings.xml b/core/ui/src/androidMain/res/values-ar-rSA/strings.xml similarity index 100% rename from core/ui/src/main/res/values-ar-rSA/strings.xml rename to core/ui/src/androidMain/res/values-ar-rSA/strings.xml diff --git a/core/ui/src/main/res/values-ar-rSA/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-ar-rSA/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-ar-rSA/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-ar-rSA/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-bg-rBG/protection.xml b/core/ui/src/androidMain/res/values-bg-rBG/protection.xml similarity index 100% rename from core/ui/src/main/res/values-bg-rBG/protection.xml rename to core/ui/src/androidMain/res/values-bg-rBG/protection.xml diff --git a/core/ui/src/main/res/values-bg-rBG/strings.xml b/core/ui/src/androidMain/res/values-bg-rBG/strings.xml similarity index 100% rename from core/ui/src/main/res/values-bg-rBG/strings.xml rename to core/ui/src/androidMain/res/values-bg-rBG/strings.xml diff --git a/core/ui/src/main/res/values-bg-rBG/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-bg-rBG/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-bg-rBG/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-bg-rBG/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-ca-rES/protection.xml b/core/ui/src/androidMain/res/values-ca-rES/protection.xml similarity index 100% rename from core/ui/src/main/res/values-ca-rES/protection.xml rename to core/ui/src/androidMain/res/values-ca-rES/protection.xml diff --git a/core/ui/src/main/res/values-ca-rES/strings.xml b/core/ui/src/androidMain/res/values-ca-rES/strings.xml similarity index 100% rename from core/ui/src/main/res/values-ca-rES/strings.xml rename to core/ui/src/androidMain/res/values-ca-rES/strings.xml diff --git a/core/ui/src/main/res/values-ca-rES/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-ca-rES/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-ca-rES/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-ca-rES/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-cs-rCZ/protection.xml b/core/ui/src/androidMain/res/values-cs-rCZ/protection.xml similarity index 100% rename from core/ui/src/main/res/values-cs-rCZ/protection.xml rename to core/ui/src/androidMain/res/values-cs-rCZ/protection.xml diff --git a/core/ui/src/main/res/values-cs-rCZ/strings.xml b/core/ui/src/androidMain/res/values-cs-rCZ/strings.xml similarity index 100% rename from core/ui/src/main/res/values-cs-rCZ/strings.xml rename to core/ui/src/androidMain/res/values-cs-rCZ/strings.xml diff --git a/core/ui/src/main/res/values-cs-rCZ/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-cs-rCZ/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-cs-rCZ/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-cs-rCZ/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-cy-rGB/protection.xml b/core/ui/src/androidMain/res/values-cy-rGB/protection.xml similarity index 100% rename from core/ui/src/main/res/values-cy-rGB/protection.xml rename to core/ui/src/androidMain/res/values-cy-rGB/protection.xml diff --git a/core/ui/src/main/res/values-da-rDK/protection.xml b/core/ui/src/androidMain/res/values-da-rDK/protection.xml similarity index 100% rename from core/ui/src/main/res/values-da-rDK/protection.xml rename to core/ui/src/androidMain/res/values-da-rDK/protection.xml diff --git a/core/ui/src/main/res/values-da-rDK/strings.xml b/core/ui/src/androidMain/res/values-da-rDK/strings.xml similarity index 100% rename from core/ui/src/main/res/values-da-rDK/strings.xml rename to core/ui/src/androidMain/res/values-da-rDK/strings.xml diff --git a/core/ui/src/main/res/values-da-rDK/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-da-rDK/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-da-rDK/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-da-rDK/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-de-rDE/protection.xml b/core/ui/src/androidMain/res/values-de-rDE/protection.xml similarity index 100% rename from core/ui/src/main/res/values-de-rDE/protection.xml rename to core/ui/src/androidMain/res/values-de-rDE/protection.xml diff --git a/core/ui/src/main/res/values-de-rDE/strings.xml b/core/ui/src/androidMain/res/values-de-rDE/strings.xml similarity index 100% rename from core/ui/src/main/res/values-de-rDE/strings.xml rename to core/ui/src/androidMain/res/values-de-rDE/strings.xml diff --git a/core/ui/src/main/res/values-de-rDE/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-de-rDE/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-de-rDE/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-de-rDE/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-el-rGR/protection.xml b/core/ui/src/androidMain/res/values-el-rGR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-el-rGR/protection.xml rename to core/ui/src/androidMain/res/values-el-rGR/protection.xml diff --git a/core/ui/src/main/res/values-el-rGR/strings.xml b/core/ui/src/androidMain/res/values-el-rGR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-el-rGR/strings.xml rename to core/ui/src/androidMain/res/values-el-rGR/strings.xml diff --git a/core/ui/src/main/res/values-el-rGR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-el-rGR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-el-rGR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-el-rGR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-es-rES/protection.xml b/core/ui/src/androidMain/res/values-es-rES/protection.xml similarity index 100% rename from core/ui/src/main/res/values-es-rES/protection.xml rename to core/ui/src/androidMain/res/values-es-rES/protection.xml diff --git a/core/ui/src/main/res/values-es-rES/strings.xml b/core/ui/src/androidMain/res/values-es-rES/strings.xml similarity index 100% rename from core/ui/src/main/res/values-es-rES/strings.xml rename to core/ui/src/androidMain/res/values-es-rES/strings.xml diff --git a/core/ui/src/main/res/values-es-rES/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-es-rES/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-es-rES/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-es-rES/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-fi-rFI/protection.xml b/core/ui/src/androidMain/res/values-fi-rFI/protection.xml similarity index 100% rename from core/ui/src/main/res/values-fi-rFI/protection.xml rename to core/ui/src/androidMain/res/values-fi-rFI/protection.xml diff --git a/core/ui/src/main/res/values-fr-rFR/protection.xml b/core/ui/src/androidMain/res/values-fr-rFR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-fr-rFR/protection.xml rename to core/ui/src/androidMain/res/values-fr-rFR/protection.xml diff --git a/core/ui/src/main/res/values-fr-rFR/strings.xml b/core/ui/src/androidMain/res/values-fr-rFR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-fr-rFR/strings.xml rename to core/ui/src/androidMain/res/values-fr-rFR/strings.xml diff --git a/core/ui/src/main/res/values-fr-rFR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-fr-rFR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-fr-rFR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-fr-rFR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-hr-rHR/protection.xml b/core/ui/src/androidMain/res/values-hr-rHR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-hr-rHR/protection.xml rename to core/ui/src/androidMain/res/values-hr-rHR/protection.xml diff --git a/core/ui/src/main/res/values-hr-rHR/strings.xml b/core/ui/src/androidMain/res/values-hr-rHR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-hr-rHR/strings.xml rename to core/ui/src/androidMain/res/values-hr-rHR/strings.xml diff --git a/core/ui/src/main/res/values-hr-rHR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-hr-rHR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-hr-rHR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-hr-rHR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-hu-rHU/protection.xml b/core/ui/src/androidMain/res/values-hu-rHU/protection.xml similarity index 100% rename from core/ui/src/main/res/values-hu-rHU/protection.xml rename to core/ui/src/androidMain/res/values-hu-rHU/protection.xml diff --git a/core/ui/src/main/res/values-hu-rHU/strings.xml b/core/ui/src/androidMain/res/values-hu-rHU/strings.xml similarity index 100% rename from core/ui/src/main/res/values-hu-rHU/strings.xml rename to core/ui/src/androidMain/res/values-hu-rHU/strings.xml diff --git a/core/ui/src/main/res/values-hu-rHU/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-hu-rHU/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-hu-rHU/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-hu-rHU/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-it-rIT/protection.xml b/core/ui/src/androidMain/res/values-it-rIT/protection.xml similarity index 100% rename from core/ui/src/main/res/values-it-rIT/protection.xml rename to core/ui/src/androidMain/res/values-it-rIT/protection.xml diff --git a/core/ui/src/main/res/values-it-rIT/strings.xml b/core/ui/src/androidMain/res/values-it-rIT/strings.xml similarity index 100% rename from core/ui/src/main/res/values-it-rIT/strings.xml rename to core/ui/src/androidMain/res/values-it-rIT/strings.xml diff --git a/core/ui/src/main/res/values-it-rIT/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-it-rIT/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-it-rIT/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-it-rIT/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-iw-rIL/protection.xml b/core/ui/src/androidMain/res/values-iw-rIL/protection.xml similarity index 100% rename from core/ui/src/main/res/values-iw-rIL/protection.xml rename to core/ui/src/androidMain/res/values-iw-rIL/protection.xml diff --git a/core/ui/src/main/res/values-iw-rIL/strings.xml b/core/ui/src/androidMain/res/values-iw-rIL/strings.xml similarity index 100% rename from core/ui/src/main/res/values-iw-rIL/strings.xml rename to core/ui/src/androidMain/res/values-iw-rIL/strings.xml diff --git a/core/ui/src/main/res/values-iw-rIL/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-iw-rIL/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-iw-rIL/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-iw-rIL/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-ko-rKR/protection.xml b/core/ui/src/androidMain/res/values-ko-rKR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-ko-rKR/protection.xml rename to core/ui/src/androidMain/res/values-ko-rKR/protection.xml diff --git a/core/ui/src/main/res/values-ko-rKR/strings.xml b/core/ui/src/androidMain/res/values-ko-rKR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-ko-rKR/strings.xml rename to core/ui/src/androidMain/res/values-ko-rKR/strings.xml diff --git a/core/ui/src/main/res/values-ko-rKR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-ko-rKR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-ko-rKR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-ko-rKR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-lt-rLT/protection.xml b/core/ui/src/androidMain/res/values-lt-rLT/protection.xml similarity index 100% rename from core/ui/src/main/res/values-lt-rLT/protection.xml rename to core/ui/src/androidMain/res/values-lt-rLT/protection.xml diff --git a/core/ui/src/main/res/values-lt-rLT/strings.xml b/core/ui/src/androidMain/res/values-lt-rLT/strings.xml similarity index 100% rename from core/ui/src/main/res/values-lt-rLT/strings.xml rename to core/ui/src/androidMain/res/values-lt-rLT/strings.xml diff --git a/core/ui/src/main/res/values-lt-rLT/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-lt-rLT/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-lt-rLT/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-lt-rLT/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-nb-rNO/protection.xml b/core/ui/src/androidMain/res/values-nb-rNO/protection.xml similarity index 100% rename from core/ui/src/main/res/values-nb-rNO/protection.xml rename to core/ui/src/androidMain/res/values-nb-rNO/protection.xml diff --git a/core/ui/src/main/res/values-nb-rNO/strings.xml b/core/ui/src/androidMain/res/values-nb-rNO/strings.xml similarity index 100% rename from core/ui/src/main/res/values-nb-rNO/strings.xml rename to core/ui/src/androidMain/res/values-nb-rNO/strings.xml diff --git a/core/ui/src/main/res/values-nb-rNO/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-nb-rNO/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-nb-rNO/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-nb-rNO/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-night/colors.xml b/core/ui/src/androidMain/res/values-night/colors.xml similarity index 100% rename from core/ui/src/main/res/values-night/colors.xml rename to core/ui/src/androidMain/res/values-night/colors.xml diff --git a/core/ui/src/main/res/values-night/styles.xml b/core/ui/src/androidMain/res/values-night/styles.xml similarity index 100% rename from core/ui/src/main/res/values-night/styles.xml rename to core/ui/src/androidMain/res/values-night/styles.xml diff --git a/core/ui/src/main/res/values-nl-rNL/protection.xml b/core/ui/src/androidMain/res/values-nl-rNL/protection.xml similarity index 100% rename from core/ui/src/main/res/values-nl-rNL/protection.xml rename to core/ui/src/androidMain/res/values-nl-rNL/protection.xml diff --git a/core/ui/src/main/res/values-nl-rNL/strings.xml b/core/ui/src/androidMain/res/values-nl-rNL/strings.xml similarity index 100% rename from core/ui/src/main/res/values-nl-rNL/strings.xml rename to core/ui/src/androidMain/res/values-nl-rNL/strings.xml diff --git a/core/ui/src/main/res/values-nl-rNL/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-nl-rNL/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-nl-rNL/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-nl-rNL/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-pl-rPL/protection.xml b/core/ui/src/androidMain/res/values-pl-rPL/protection.xml similarity index 100% rename from core/ui/src/main/res/values-pl-rPL/protection.xml rename to core/ui/src/androidMain/res/values-pl-rPL/protection.xml diff --git a/core/ui/src/main/res/values-pl-rPL/strings.xml b/core/ui/src/androidMain/res/values-pl-rPL/strings.xml similarity index 100% rename from core/ui/src/main/res/values-pl-rPL/strings.xml rename to core/ui/src/androidMain/res/values-pl-rPL/strings.xml diff --git a/core/ui/src/main/res/values-pl-rPL/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-pl-rPL/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-pl-rPL/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-pl-rPL/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-pt-rBR/protection.xml b/core/ui/src/androidMain/res/values-pt-rBR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rBR/protection.xml rename to core/ui/src/androidMain/res/values-pt-rBR/protection.xml diff --git a/core/ui/src/main/res/values-pt-rBR/strings.xml b/core/ui/src/androidMain/res/values-pt-rBR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rBR/strings.xml rename to core/ui/src/androidMain/res/values-pt-rBR/strings.xml diff --git a/core/ui/src/main/res/values-pt-rBR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-pt-rBR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rBR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-pt-rBR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-pt-rPT/protection.xml b/core/ui/src/androidMain/res/values-pt-rPT/protection.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rPT/protection.xml rename to core/ui/src/androidMain/res/values-pt-rPT/protection.xml diff --git a/core/ui/src/main/res/values-pt-rPT/strings.xml b/core/ui/src/androidMain/res/values-pt-rPT/strings.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rPT/strings.xml rename to core/ui/src/androidMain/res/values-pt-rPT/strings.xml diff --git a/core/ui/src/main/res/values-pt-rPT/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-pt-rPT/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-pt-rPT/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-pt-rPT/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-ro-rRO/protection.xml b/core/ui/src/androidMain/res/values-ro-rRO/protection.xml similarity index 100% rename from core/ui/src/main/res/values-ro-rRO/protection.xml rename to core/ui/src/androidMain/res/values-ro-rRO/protection.xml diff --git a/core/ui/src/main/res/values-ro-rRO/strings.xml b/core/ui/src/androidMain/res/values-ro-rRO/strings.xml similarity index 100% rename from core/ui/src/main/res/values-ro-rRO/strings.xml rename to core/ui/src/androidMain/res/values-ro-rRO/strings.xml diff --git a/core/ui/src/main/res/values-ro-rRO/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-ro-rRO/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-ro-rRO/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-ro-rRO/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-ru-rRU/protection.xml b/core/ui/src/androidMain/res/values-ru-rRU/protection.xml similarity index 100% rename from core/ui/src/main/res/values-ru-rRU/protection.xml rename to core/ui/src/androidMain/res/values-ru-rRU/protection.xml diff --git a/core/ui/src/main/res/values-ru-rRU/strings.xml b/core/ui/src/androidMain/res/values-ru-rRU/strings.xml similarity index 100% rename from core/ui/src/main/res/values-ru-rRU/strings.xml rename to core/ui/src/androidMain/res/values-ru-rRU/strings.xml diff --git a/core/ui/src/main/res/values-ru-rRU/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-ru-rRU/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-ru-rRU/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-ru-rRU/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-sk-rSK/protection.xml b/core/ui/src/androidMain/res/values-sk-rSK/protection.xml similarity index 100% rename from core/ui/src/main/res/values-sk-rSK/protection.xml rename to core/ui/src/androidMain/res/values-sk-rSK/protection.xml diff --git a/core/ui/src/main/res/values-sk-rSK/strings.xml b/core/ui/src/androidMain/res/values-sk-rSK/strings.xml similarity index 100% rename from core/ui/src/main/res/values-sk-rSK/strings.xml rename to core/ui/src/androidMain/res/values-sk-rSK/strings.xml diff --git a/core/ui/src/main/res/values-sk-rSK/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-sk-rSK/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-sk-rSK/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-sk-rSK/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-sl-rSI/protection.xml b/core/ui/src/androidMain/res/values-sl-rSI/protection.xml similarity index 100% rename from core/ui/src/main/res/values-sl-rSI/protection.xml rename to core/ui/src/androidMain/res/values-sl-rSI/protection.xml diff --git a/core/ui/src/main/res/values-sr-rCS/protection.xml b/core/ui/src/androidMain/res/values-sr-rCS/protection.xml similarity index 100% rename from core/ui/src/main/res/values-sr-rCS/protection.xml rename to core/ui/src/androidMain/res/values-sr-rCS/protection.xml diff --git a/core/ui/src/main/res/values-sr-rCS/strings.xml b/core/ui/src/androidMain/res/values-sr-rCS/strings.xml similarity index 100% rename from core/ui/src/main/res/values-sr-rCS/strings.xml rename to core/ui/src/androidMain/res/values-sr-rCS/strings.xml diff --git a/core/ui/src/main/res/values-sr-rCS/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-sr-rCS/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-sr-rCS/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-sr-rCS/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-sv-rSE/protection.xml b/core/ui/src/androidMain/res/values-sv-rSE/protection.xml similarity index 100% rename from core/ui/src/main/res/values-sv-rSE/protection.xml rename to core/ui/src/androidMain/res/values-sv-rSE/protection.xml diff --git a/core/ui/src/main/res/values-sv-rSE/strings.xml b/core/ui/src/androidMain/res/values-sv-rSE/strings.xml similarity index 100% rename from core/ui/src/main/res/values-sv-rSE/strings.xml rename to core/ui/src/androidMain/res/values-sv-rSE/strings.xml diff --git a/core/ui/src/main/res/values-sv-rSE/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-sv-rSE/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-sv-rSE/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-sv-rSE/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-sw600dp/layout.xml b/core/ui/src/androidMain/res/values-sw600dp/layout.xml similarity index 100% rename from core/ui/src/main/res/values-sw600dp/layout.xml rename to core/ui/src/androidMain/res/values-sw600dp/layout.xml diff --git a/core/ui/src/main/res/values-tr-rTR/protection.xml b/core/ui/src/androidMain/res/values-tr-rTR/protection.xml similarity index 100% rename from core/ui/src/main/res/values-tr-rTR/protection.xml rename to core/ui/src/androidMain/res/values-tr-rTR/protection.xml diff --git a/core/ui/src/main/res/values-tr-rTR/strings.xml b/core/ui/src/androidMain/res/values-tr-rTR/strings.xml similarity index 100% rename from core/ui/src/main/res/values-tr-rTR/strings.xml rename to core/ui/src/androidMain/res/values-tr-rTR/strings.xml diff --git a/core/ui/src/main/res/values-tr-rTR/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-tr-rTR/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-tr-rTR/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-tr-rTR/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-uk-rUA/protection.xml b/core/ui/src/androidMain/res/values-uk-rUA/protection.xml similarity index 100% rename from core/ui/src/main/res/values-uk-rUA/protection.xml rename to core/ui/src/androidMain/res/values-uk-rUA/protection.xml diff --git a/core/ui/src/main/res/values-uk-rUA/strings.xml b/core/ui/src/androidMain/res/values-uk-rUA/strings.xml similarity index 100% rename from core/ui/src/main/res/values-uk-rUA/strings.xml rename to core/ui/src/androidMain/res/values-uk-rUA/strings.xml diff --git a/core/ui/src/main/res/values-uk-rUA/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-uk-rUA/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-uk-rUA/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-uk-rUA/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-vi-rVN/protection.xml b/core/ui/src/androidMain/res/values-vi-rVN/protection.xml similarity index 100% rename from core/ui/src/main/res/values-vi-rVN/protection.xml rename to core/ui/src/androidMain/res/values-vi-rVN/protection.xml diff --git a/core/ui/src/main/res/values-vi-rVN/strings.xml b/core/ui/src/androidMain/res/values-vi-rVN/strings.xml similarity index 100% rename from core/ui/src/main/res/values-vi-rVN/strings.xml rename to core/ui/src/androidMain/res/values-vi-rVN/strings.xml diff --git a/core/ui/src/main/res/values-vi-rVN/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-vi-rVN/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-vi-rVN/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-vi-rVN/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-zh-rCN/protection.xml b/core/ui/src/androidMain/res/values-zh-rCN/protection.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rCN/protection.xml rename to core/ui/src/androidMain/res/values-zh-rCN/protection.xml diff --git a/core/ui/src/main/res/values-zh-rCN/strings.xml b/core/ui/src/androidMain/res/values-zh-rCN/strings.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rCN/strings.xml rename to core/ui/src/androidMain/res/values-zh-rCN/strings.xml diff --git a/core/ui/src/main/res/values-zh-rCN/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-zh-rCN/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rCN/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-zh-rCN/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values-zh-rTW/protection.xml b/core/ui/src/androidMain/res/values-zh-rTW/protection.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rTW/protection.xml rename to core/ui/src/androidMain/res/values-zh-rTW/protection.xml diff --git a/core/ui/src/main/res/values-zh-rTW/strings.xml b/core/ui/src/androidMain/res/values-zh-rTW/strings.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rTW/strings.xml rename to core/ui/src/androidMain/res/values-zh-rTW/strings.xml diff --git a/core/ui/src/main/res/values-zh-rTW/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values-zh-rTW/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values-zh-rTW/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values-zh-rTW/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values/colors.xml b/core/ui/src/androidMain/res/values/colors.xml similarity index 100% rename from core/ui/src/main/res/values/colors.xml rename to core/ui/src/androidMain/res/values/colors.xml diff --git a/core/ui/src/main/res/values/layout.xml b/core/ui/src/androidMain/res/values/layout.xml similarity index 100% rename from core/ui/src/main/res/values/layout.xml rename to core/ui/src/androidMain/res/values/layout.xml diff --git a/core/ui/src/main/res/values/protection.xml b/core/ui/src/androidMain/res/values/protection.xml similarity index 100% rename from core/ui/src/main/res/values/protection.xml rename to core/ui/src/androidMain/res/values/protection.xml diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/androidMain/res/values/strings.xml similarity index 100% rename from core/ui/src/main/res/values/strings.xml rename to core/ui/src/androidMain/res/values/strings.xml diff --git a/core/ui/src/main/res/values/strings_scene_wizard.xml b/core/ui/src/androidMain/res/values/strings_scene_wizard.xml similarity index 100% rename from core/ui/src/main/res/values/strings_scene_wizard.xml rename to core/ui/src/androidMain/res/values/strings_scene_wizard.xml diff --git a/core/ui/src/main/res/values/styles.xml b/core/ui/src/androidMain/res/values/styles.xml similarity index 100% rename from core/ui/src/main/res/values/styles.xml rename to core/ui/src/androidMain/res/values/styles.xml diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/UiMode.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/UiMode.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/UiMode.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/UiMode.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsCard.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsCard.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsCard.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsCard.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsFab.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsFab.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsFab.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsFab.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSpacing.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSpacing.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsSpacing.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSpacing.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTopAppBar.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBar.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/AapsTopAppBar.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBar.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ComposablePluginContent.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ComposablePluginContent.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ComposablePluginContent.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ComposablePluginContent.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ComposeScreenContent.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ComposeScreenContent.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ComposeScreenContent.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ComposeScreenContent.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/GeneralColors.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/GeneralColors.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/GeneralColors.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/GeneralColors.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/Modifiers.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/Modifiers.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/Modifiers.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/Modifiers.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProfileHelperColors.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ProfileHelperColors.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ProfileHelperColors.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ProfileHelperColors.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/QuickAddButtons.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/QuickAddButtons.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/QuickAddButtons.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/QuickAddButtons.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ScreenMode.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ScreenMode.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ScreenMode.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ScreenMode.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/SnackbarColors.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SnackbarColors.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/SnackbarColors.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SnackbarColors.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/StateColors.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/StateColors.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/StateColors.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/StateColors.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/TonalIcon.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TonalIcon.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/TonalIcon.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TonalIcon.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/ToolbarConfig.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ToolbarConfig.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/ToolbarConfig.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ToolbarConfig.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/banner/Banner.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/banner/Banner.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/banner/Banner.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/banner/Banner.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Careportal.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Careportal.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Careportal.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Careportal.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAaps.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAaps.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAaps.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAaps.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAction.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAction.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAction.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAction.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActivity.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActivity.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcActivity.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActivity.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncement.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncement.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncement.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncement.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDown.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDown.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDown.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDown.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUp.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUp.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUp.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUp.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlat.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlat.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlat.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlat.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDown.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDown.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDown.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDown.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUp.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUp.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUp.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUp.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalid.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalid.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalid.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalid.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeft.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeft.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeft.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeft.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDown.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDown.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDown.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDown.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUp.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUp.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUp.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUp.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowNone.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNone.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowNone.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNone.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDown.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDown.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDown.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDown.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUp.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUp.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUp.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUp.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAs.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAs.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAs.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAs.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAbove.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbove.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAbove.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbove.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveX.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveX.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveX.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveX.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowX.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowX.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowX.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowX.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsX.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsX.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAsX.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsX.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAutomation.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomation.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcAutomation.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomation.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBgCheck.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheck.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBgCheck.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheck.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBolus.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBolus.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBolus.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBolus.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBread.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBread.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcBread.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBread.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcByoda.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcByoda.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcByoda.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcByoda.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCake.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCake.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCake.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCake.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalculator.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculator.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalculator.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculator.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalibration.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibration.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCalibration.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibration.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolus.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolus.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolus.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolus.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChange.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChange.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChange.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChange.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCarbs.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbs.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCarbs.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbs.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsert.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsert.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsert.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsert.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotes.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotes.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotes.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotes.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfiles.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfiles.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfiles.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfiles.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDelta.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDelta.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDelta.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDelta.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDiaconn.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconn.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcDiaconn.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconn.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolus.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolus.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolus.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolus.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgm.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgm.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgm.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgm.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericIcon.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIcon.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGenericIcon.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIcon.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrive.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrive.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrive.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrive.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcHistory.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcHistory.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcHistory.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcHistory.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosed.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosed.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosed.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosed.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabled.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabled.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabled.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabled.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnected.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnected.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnected.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnected.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopHidden.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHidden.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopHidden.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHidden.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgs.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgs.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgs.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgs.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPaused.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPaused.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPaused.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPaused.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDst.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDst.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDst.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDst.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPump.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPump.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPump.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPump.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnect.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnect.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnect.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnect.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolus.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolus.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolus.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolus.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcMdi.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcMdi.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcMdi.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcMdi.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNoTbr.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbr.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNoTbr.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbr.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNote.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNote.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcNote.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNote.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPatchPump.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPump.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPatchPump.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPump.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPizza.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPizza.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPizza.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPizza.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomation.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomation.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomation.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomation.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotune.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotune.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotune.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotune.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginByoda.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByoda.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginByoda.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByoda.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginCombo.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginCombo.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginCombo.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginCombo.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilder.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilder.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilder.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilder.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDana.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDana.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDana.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDana.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconn.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconn.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconn.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconn.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatch.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatch.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatch.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatch.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquil.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquil.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquil.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquil.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversense.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversense.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversense.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversense.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginFood.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFood.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginFood.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFood.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarmin.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarmin.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarmin.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarmin.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimp.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimp.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimp.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimp.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovo.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovo.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovo.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovo.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsight.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsight.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsight.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsight.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulin.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulin.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulin.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulin.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligo.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligo.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligo.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligo.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenance.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenance.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenance.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenance.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronic.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronic.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronic.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronic.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrum.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrum.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrum.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrum.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640G.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640G.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640G.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640G.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClient.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClient.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClient.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClient.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBg.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBg.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBg.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBg.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectives.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectives.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectives.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectives.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipod.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipod.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipod.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipod.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenAps.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenAps.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenAps.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenAps.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumans.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumans.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumans.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumans.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTech.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTech.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTech.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTech.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBg.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBg.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBg.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBg.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSms.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSms.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSms.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSms.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyai.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyai.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyai.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyai.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobi.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobi.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobi.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobi.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepool.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepool.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepool.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepool.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizen.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizen.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomato.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomato.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomato.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomato.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPump.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPump.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPump.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPump.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcProfile.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcProfile.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcProfile.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcProfile.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpBattery.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBattery.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpBattery.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBattery.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridge.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridge.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridge.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridge.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuestion.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestion.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuestion.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestion.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizard.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizard.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizard.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizard.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOff.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOff.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOff.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOff.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizard.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizard.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizard.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizard.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotation.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotation.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotation.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotation.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSmb.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSmb.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcSmb.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSmb.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcStats.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcStats.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcStats.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcStats.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancel.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancel.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancel.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancel.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrHigh.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHigh.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrHigh.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHigh.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrLow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTbrLow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtActivity.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivity.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtActivity.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivity.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtCancel.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancel.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtCancel.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancel.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoon.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoon.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoon.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoon.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHigh.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHigh.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHigh.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHigh.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHypo.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypo.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtHypo.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypo.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtManual.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManual.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcTtManual.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManual.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcUserOptions.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptions.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcUserOptions.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptions.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcXDrip.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcXDrip.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/IcXDrip.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcXDrip.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Ns.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Ns.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Ns.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Ns.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Pump.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Pump.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/Pump.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/Pump.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBack.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFront.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBack.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFront.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBack.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFront.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatments.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatments.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatments.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatments.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenter.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenter.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenter.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenter.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlat.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlat.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlat.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlat.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginAction.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginAction.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginAction.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginAction.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilder.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilder.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilder.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilder.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverview.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverview.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverview.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverview.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementColors.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementColors.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/ElementColors.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementColors.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/NavigationRequest.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/NavigationRequest.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/navigation/NavigationRequest.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/NavigationRequest.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/BasicPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/BasicPreference.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/BasicPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/BasicPreference.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/LocalPasswordCheck.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/LocalPasswordCheck.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/LocalPasswordCheck.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/LocalPasswordCheck.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PaddingValuesExtensions.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PaddingValuesExtensions.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PaddingValuesExtensions.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PaddingValuesExtensions.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceScreenContent.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSubScreenDef.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ScrollIndicators.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ScrollIndicators.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/preference/ScrollIndicators.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ScrollIndicators.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFab.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFab.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFab.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFab.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryModels.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryModels.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryModels.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryModels.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/StepProgressIndicator.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/StepProgressIndicator.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/StepProgressIndicator.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/StepProgressIndicator.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/TickerFlow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/TickerFlow.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/TickerFlow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/TickerFlow.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/WizardStepLayout.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/WizardStepLayout.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/pump/WizardStepLayout.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/WizardStepLayout.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowExtensions.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowExtensions.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowExtensions.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowExtensions.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/BodyType.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/BodyView.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ZoomableBodyDiagram.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ZoomableBodyDiagram.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/compose/siteRotation/ZoomableBodyDiagram.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ZoomableBodyDiagram.kt diff --git a/core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/elements/WeekDay.kt similarity index 100% rename from core/ui/src/main/kotlin/app/aaps/core/ui/elements/WeekDay.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/elements/WeekDay.kt diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionStartTempTarget.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionStartTempTarget.kt index 5db88977873d..1195ad189a30 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionStartTempTarget.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/actions/ActionStartTempTarget.kt @@ -12,7 +12,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.objects.extensions.friendlyDescription +import app.aaps.core.ui.extensions.friendlyDescription import app.aaps.core.ui.compose.icons.IcTtHigh import app.aaps.core.utils.JsonHelper import app.aaps.core.utils.JsonHelper.safeGetDouble diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index 084e1106481a..e178e5230b2e 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -38,7 +38,7 @@ import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.TrendCalculator import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.objects.extensions.apsAdjustedTargetMgdl -import app.aaps.core.objects.extensions.generateCOBString +import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round import app.aaps.core.objects.extensions.toStringShort import app.aaps.core.utils.DeferredForegroundStart diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/GlucoseValueExtensionKtTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/GlucoseValueExtensionKtTest.kt index db3acce424ad..7b42adbeb6e1 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/GlucoseValueExtensionKtTest.kt +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/GlucoseValueExtensionKtTest.kt @@ -5,7 +5,7 @@ import app.aaps.core.data.model.GV import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.SourceSensor import app.aaps.core.data.model.TrendArrow -import app.aaps.core.objects.extensions.directionToIcon +import app.aaps.core.ui.extensions.directionToIcon import app.aaps.core.objects.extensions.valueToUnits import app.aaps.core.ui.compose.icons.IcArrowDoubleDown import app.aaps.core.ui.compose.icons.IcArrowDoubleUp diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/TemporaryTargetExtensionKtTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/TemporaryTargetExtensionKtTest.kt index 6f070d1ab307..3c7723be85a3 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/TemporaryTargetExtensionKtTest.kt +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/extensions/TemporaryTargetExtensionKtTest.kt @@ -3,8 +3,8 @@ package app.aaps.plugins.main.extensions import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.IDs import app.aaps.core.data.model.TT -import app.aaps.core.objects.extensions.highValueToUnitsToString -import app.aaps.core.objects.extensions.lowValueToUnitsToString +import app.aaps.core.ui.extensions.highValueToUnitsToString +import app.aaps.core.ui.extensions.lowValueToUnitsToString import app.aaps.core.objects.extensions.target import app.aaps.shared.tests.TestBaseWithProfile import com.google.common.truth.Truth.assertThat diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/compose/BgSourceScreen.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/compose/BgSourceScreen.kt index f218a69d7156..bcb6f9fc65a8 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/compose/BgSourceScreen.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/compose/BgSourceScreen.kt @@ -41,7 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.model.GV import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.objects.extensions.directionToIcon +import app.aaps.core.ui.extensions.directionToIcon import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.SelectableListToolbar diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt index a8ddd6abd81b..f3193696b66d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/smsCommunicator/SmsCommunicatorPlugin.kt @@ -53,7 +53,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.withCompose import app.aaps.core.objects.constraints.ConstraintObject -import app.aaps.core.objects.extensions.generateCOBString +import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round import app.aaps.core.objects.runningMode.PumpCommandGate import app.aaps.core.objects.runningMode.RunningModeGuard diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index 926fb65cb0ff..ba31551c2945 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -85,7 +85,7 @@ import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.apsAdjustedTargetMgdl import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.generateCOBString +import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round import app.aaps.core.objects.extensions.toStringShort import app.aaps.core.objects.extensions.valueToUnits diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 2c375db1864d..08beb3ec411d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -44,7 +44,7 @@ import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.objects.extensions.generateCOBString +import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round import app.aaps.core.objects.extensions.toStringShort import app.aaps.core.objects.profile.ProfileSealed diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/chips/ChipsViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/chips/ChipsViewModel.kt index 0b4f438c0172..c9d8dacec7a3 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/chips/ChipsViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/chips/ChipsViewModel.kt @@ -22,7 +22,7 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.keys.BooleanNonKey import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.objects.extensions.displayText +import app.aaps.core.ui.extensions.displayText import app.aaps.core.objects.extensions.round import app.aaps.core.ui.R import dagger.assisted.Assisted diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt index b6ce16c865d7..edd2cc3450f2 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/TempTargetScreen.kt @@ -35,8 +35,8 @@ import app.aaps.core.data.time.T import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.Translator -import app.aaps.core.objects.extensions.highValueToUnitsToString -import app.aaps.core.objects.extensions.lowValueToUnitsToString +import app.aaps.core.ui.extensions.highValueToUnitsToString +import app.aaps.core.ui.extensions.lowValueToUnitsToString import app.aaps.core.ui.R as CoreUiR import app.aaps.core.ui.compose.AapsCard import app.aaps.core.ui.compose.AapsTheme diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModel.kt index 1c4f324c0dde..a689719ee5fe 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempTargetViewModel.kt @@ -18,7 +18,7 @@ import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.objects.extensions.friendlyDescription +import app.aaps.core.ui.extensions.friendlyDescription import app.aaps.core.ui.R import app.aaps.core.ui.compose.SelectableListToolbar import app.aaps.core.ui.compose.ToolbarConfig diff --git a/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt b/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt index b3f305dd1788..da3023b5c0ba 100644 --- a/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt +++ b/ui/src/main/kotlin/app/aaps/ui/widget/glance/WidgetStateLoader.kt @@ -24,7 +24,7 @@ import app.aaps.core.keys.BooleanComposedKey import app.aaps.core.keys.IntComposedKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.apsAdjustedTargetMgdl -import app.aaps.core.objects.extensions.displayText +import app.aaps.core.ui.extensions.displayText import app.aaps.core.objects.extensions.round import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.compose.DarkGeneralColors From feddd57589d03982999214cf18c41859402e5979 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 13:20:13 +0200 Subject: [PATCH 117/146] HTML becomes AnnotatedString, and :core:ui reaches 348 commonMain Two halves. They cannot be split into buildable commits, because the source-set sweep rewrote the path of nearly every file the first half edits. ## HTML Only and
appear anywhere - 36 and 4 across every English string resource, nothing else - so a 60 line htmlToAnnotatedString() in commonMain replaces AnnotatedString.fromHtml, which is Android only and was what kept the dialogs out of commonMain. 11 unit tests cover it, including the cases the platform parser gets wrong: an unknown tag and a bare < are kept rather than swallowed, and an unbalanced still terminates. The rule applied to every other site: markup is built where the text is generated, not parsed back out before it is shown. - AutomationRuntime.executionLog is List. It built "${event.title}:" per entry, so making the screen render plain text would have shown a literal - the log panel is the one place formatting had to move to the generator rather than simply disappear. - MaintenanceDialogs, NSClientComposeContent and InsulinManagementScreen build their bold headers with buildAnnotatedString and use the AnnotatedString overload the dialogs already had. - AppRepository.cleanupDatabase joins with a newline instead of
. - NSDeviceStatusHandler wrote "key: value
" and the overview stripped every tag straight back out before displaying it. It now writes plain lines. Nothing AAPS uploads contains markup - the Nightscout pump.extended object is built from versions, dates and rates - so no outbound formatting was lost. Inbound is different: pump.extended read FROM Nightscout is written by whatever uploaded it, so the flattening the overview used to do is kept, moved to the point the foreign data arrives. Deleted as dead: HtmlHelper (its only callers were two Insight sites, and the Insight alert strings contain no markup at all), EventTidepoolStatus .toPreparedHtml, and PumpEnactResult.toHtml with its tests - no production caller for any of them. Also an unclosed in an Omnipod Dash notification, which was rendering literally because notification text is drawn by a plain Text. ## commonMain 166 -> 348 of 442 files. - stringResource(TextRef) is expect/actual. The Android actual is the previous code unchanged; iOS gets a documented placeholder, because expect needs an actual on every target and that is what keeps the iOS compile honest. Every screen funnels through this, so nothing else could move until it did. - 174 @Preview files use org.jetbrains.compose ... .Preview. It accepts showBackground, name and widthDp, which is every parameter used here - verified by compiling for iOS, with a bogus parameter as a negative control. 148 moved; the other 26 preview components that are still Android bound. - 49 dead `import app.aaps.core.ui.R` lines. Files already converted to UiStrings kept the import. Invisible in an Android module, a hard error in commonMain. - SearchableItem used javaClass.simpleName; pluginId is the documented stable identity and defaults to exactly that string. No plugin overrides it. - includeFontPadding is an Android text-layout quirk, now a one line expect. 24 root blockers remain, 44 more files wait only on those. Six need ResourceHelper narrowed to TextResolver, two carry @StringRes, two still call the platform stringResource; the rest are Activity, Context, Locale, permissions or java.math.BigDecimal and want expect/actual rather than migration. --- core/ui/build.gradle.kts | 1 + .../app/aaps/core/ui/compose/HtmlTextTest.kt | 68 +++++ .../ui/clientcontrol/FailureReasonText.kt | 1 - .../app/aaps/core/ui/compose/CarbTimeRow.kt | 1 - .../core/ui/compose/FontPadding.android.kt | 5 + .../core/ui/compose/MasterOfflineBanner.kt | 1 - .../aaps/core/ui/compose/NumberInputRow.kt | 1 - .../core/ui/compose/NumberInputRowPreviews.kt | 3 +- .../core/ui/compose/PluginCategoryTitle.kt | 1 - .../ui/compose/SliderWithButtonsPreviews.kt | 3 +- ...Resource.kt => TextRefResource.android.kt} | 27 +- .../aaps/core/ui/compose/TimeRangePicker.kt | 1 - .../ui/compose/dialogs/GlobalSnackbarHost.kt | 1 - .../ui/compose/dialogs/ValueInputDialog.kt | 1 - .../dialogs/ValueInputDialogPreviews.kt | 2 +- .../compose/insulin/ConcentrationDropDown.kt | 1 - .../core/ui/compose/insulin/SelectInsulin.kt | 1 - .../compose/insulin/SelectInsulinPreviews.kt | 2 +- .../ui/compose/navigation/ElementTypeStyle.kt | 1 - .../preference/AdaptiveDoublePreference.kt | 1 - .../AdaptiveDoublePreferencePreviews.kt | 2 +- .../preference/AdaptiveIntPreference.kt | 1 - .../AdaptiveIntPreferencePreviews.kt | 2 +- .../AdaptiveListPreferencePreviews.kt | 2 +- .../AdaptiveMasterPasswordPreference.kt | 1 - ...daptiveMasterPasswordPreferencePreviews.kt | 2 +- .../preference/AdaptivePasswordPreference.kt | 1 - .../AdaptivePasswordPreferencePreviews.kt | 2 +- .../preference/AdaptiveStringPreference.kt | 1 - .../AdaptiveStringPreferencePreviews.kt | 2 +- .../preference/AdaptiveSwitchPreference.kt | 1 - .../AdaptiveSwitchPreferencePreviews.kt | 2 +- .../CollapsibleCardSectionContentPreviews.kt | 3 +- .../preference/ListPreferencePreviews.kt | 2 +- .../preference/PreferenceCategoryPreviews.kt | 2 +- .../compose/preference/PreferencePreviews.kt | 2 +- .../preference/SwitchPreferencePreviews.kt | 2 +- .../core/ui/compose/preference/SyncBadge.kt | 1 - .../preference/TextFieldPreferencePreviews.kt | 2 +- .../core/ui/compose/pump/BlePreCheckHost.kt | 1 - .../ui/compose/siteRotation/SiteEntryList.kt | 1 - .../siteRotation/SiteEntryListPreviews.kt | 2 +- .../siteRotation/SiteLocationPicker.kt | 1 - .../SiteLocationPickerPreviews.kt | 2 +- .../siteRotation/SiteLocationPickerScreen.kt | 1 - .../SiteLocationPickerScreenPreviews.kt | 2 +- .../siteRotation/SiteLocationWizardStep.kt | 1 - .../SiteLocationWizardStepPreviews.kt | 2 +- .../app/aaps/core/ui/search/SearchableItem.kt | 4 +- .../aaps/core/ui/compose/AapsCardPreviews.kt | 2 +- .../aaps/core/ui/compose/AapsFabPreviews.kt | 2 +- .../aaps/core/ui/compose/AapsSearchField.kt | 1 - .../ui/compose/AapsSearchFieldPreviews.kt | 2 +- .../core/ui/compose/AapsTopAppBarPreviews.kt | 2 +- .../aaps/core/ui/compose/AapsTypography.kt | 3 +- .../aaps/core/ui/compose/ConfigPluginCard.kt | 1 - .../aaps/core/ui/compose/DateTimeSection.kt | 1 - .../app/aaps/core/ui/compose/EventTimeRow.kt | 1 - .../app/aaps/core/ui/compose/FontPadding.kt | 13 + .../app/aaps/core/ui/compose/HtmlText.kt | 80 +++++ .../aaps/core/ui/compose/InsulinSelector.kt | 1 - .../ui/compose/QuickAddButtonsPreviews.kt | 2 +- .../aaps/core/ui/compose/TextRefResource.kt | 30 ++ .../app/aaps/core/ui/compose/UnitTypeText.kt | 1 - .../core/ui/compose/banner/BannerPreviews.kt | 2 +- .../ui/compose/dialogs/DatePickerModal.kt | 1 - .../dialogs/DatePickerModalPreviews.kt | 2 +- .../core/ui/compose/dialogs/ErrorDialog.kt | 5 +- .../ui/compose/dialogs/ErrorDialogPreviews.kt | 2 +- .../core/ui/compose/dialogs/OkCancelDialog.kt | 5 +- .../compose/dialogs/OkCancelDialogPreviews.kt | 2 +- .../aaps/core/ui/compose/dialogs/OkDialog.kt | 5 +- .../ui/compose/dialogs/OkDialogPreviews.kt | 2 +- .../compose/dialogs/QueryAnyPasswordDialog.kt | 1 - .../dialogs/QueryAnyPasswordDialogPreviews.kt | 2 +- .../ui/compose/dialogs/QueryPasswordDialog.kt | 1 - .../dialogs/QueryPasswordDialogPreviews.kt | 2 +- .../ui/compose/dialogs/SetPasswordDialog.kt | 1 - .../dialogs/SetPasswordDialogPreviews.kt | 2 +- .../ui/compose/dialogs/ThreeButtonDialog.kt | 5 +- .../dialogs/ThreeButtonDialogPreviews.kt | 2 +- .../ui/compose/dialogs/TimePickerModal.kt | 1 - .../dialogs/TimePickerModalPreviews.kt | 2 +- .../ui/compose/dialogs/UnifiedAuthDialog.kt | 1 - .../ui/compose/dialogs/YesNoCancelDialog.kt | 5 +- .../dialogs/YesNoCancelDialogPreviews.kt | 2 +- .../ui/compose/icons/CareportalPreviews.kt | 2 +- .../core/ui/compose/icons/IcAapsPreviews.kt | 2 +- .../core/ui/compose/icons/IcActionPreviews.kt | 2 +- .../ui/compose/icons/IcActivityPreviews.kt | 2 +- .../compose/icons/IcAnnouncementPreviews.kt | 2 +- .../icons/IcArrowDoubleDownPreviews.kt | 2 +- .../compose/icons/IcArrowDoubleUpPreviews.kt | 2 +- .../ui/compose/icons/IcArrowFlatPreviews.kt | 2 +- .../icons/IcArrowFortyFiveDownPreviews.kt | 2 +- .../icons/IcArrowFortyFiveUpPreviews.kt | 2 +- .../compose/icons/IcArrowInvalidPreviews.kt | 2 +- .../compose/icons/IcArrowLeftDownPreviews.kt | 2 +- .../ui/compose/icons/IcArrowLeftPreviews.kt | 2 +- .../ui/compose/icons/IcArrowLeftUpPreviews.kt | 2 +- .../ui/compose/icons/IcArrowNonePreviews.kt | 2 +- .../icons/IcArrowSimpleDownPreviews.kt | 2 +- .../compose/icons/IcArrowSimpleUpPreviews.kt | 2 +- .../ui/compose/icons/IcAsAbovePreviews.kt | 2 +- .../ui/compose/icons/IcAsAboveXPreviews.kt | 2 +- .../ui/compose/icons/IcAsBelowPreviews.kt | 2 +- .../ui/compose/icons/IcAsBelowXPreviews.kt | 2 +- .../core/ui/compose/icons/IcAsPreviews.kt | 2 +- .../core/ui/compose/icons/IcAsXPreviews.kt | 2 +- .../ui/compose/icons/IcAutomationPreviews.kt | 2 +- .../ui/compose/icons/IcBgCheckPreviews.kt | 2 +- .../core/ui/compose/icons/IcBolusPreviews.kt | 2 +- .../core/ui/compose/icons/IcBreadPreviews.kt | 2 +- .../core/ui/compose/icons/IcByodaPreviews.kt | 2 +- .../core/ui/compose/icons/IcCakePreviews.kt | 2 +- .../ui/compose/icons/IcCalculatorPreviews.kt | 2 +- .../ui/compose/icons/IcCalibrationPreviews.kt | 2 +- .../icons/IcCancelExtendedBolusPreviews.kt | 2 +- .../compose/icons/IcCannulaChangePreviews.kt | 2 +- .../core/ui/compose/icons/IcCarbsPreviews.kt | 2 +- .../ui/compose/icons/IcCgmInsertPreviews.kt | 2 +- .../compose/icons/IcClinicalNotesPreviews.kt | 2 +- .../icons/IcCompareProfilesPreviews.kt | 2 +- .../core/ui/compose/icons/IcDeltaPreviews.kt | 2 +- .../ui/compose/icons/IcDiaconnPreviews.kt | 2 +- .../compose/icons/IcExtendedBolusPreviews.kt | 2 +- .../ui/compose/icons/IcGenericCgmPreviews.kt | 2 +- .../ui/compose/icons/IcGenericIconPreviews.kt | 2 +- .../ui/compose/icons/IcGoogleDrivePreviews.kt | 2 +- .../ui/compose/icons/IcHistoryPreviews.kt | 2 +- .../ui/compose/icons/IcLoopClosedPreviews.kt | 2 +- .../compose/icons/IcLoopDisabledPreviews.kt | 2 +- .../icons/IcLoopDisconnectedPreviews.kt | 2 +- .../ui/compose/icons/IcLoopHiddenPreviews.kt | 2 +- .../ui/compose/icons/IcLoopLgsPreviews.kt | 2 +- .../ui/compose/icons/IcLoopOpenPreviews.kt | 2 +- .../compose/icons/IcLoopPausedDstPreviews.kt | 2 +- .../ui/compose/icons/IcLoopPausedPreviews.kt | 2 +- .../compose/icons/IcLoopPausedPumpPreviews.kt | 2 +- .../compose/icons/IcLoopReconnectPreviews.kt | 2 +- .../compose/icons/IcLoopSuperBolusPreviews.kt | 2 +- .../core/ui/compose/icons/IcMdiPreviews.kt | 2 +- .../core/ui/compose/icons/IcNoTbrPreviews.kt | 2 +- .../core/ui/compose/icons/IcNotePreviews.kt | 2 +- .../ui/compose/icons/IcPatchPumpPreviews.kt | 2 +- .../core/ui/compose/icons/IcPizzaPreviews.kt | 2 +- .../icons/IcPluginAutomationPreviews.kt | 2 +- .../compose/icons/IcPluginAutotunePreviews.kt | 2 +- .../ui/compose/icons/IcPluginByodaPreviews.kt | 2 +- .../ui/compose/icons/IcPluginComboPreviews.kt | 2 +- .../icons/IcPluginConfigBuilderPreviews.kt | 2 +- .../ui/compose/icons/IcPluginDanaPreviews.kt | 2 +- .../compose/icons/IcPluginDiaconnPreviews.kt | 2 +- .../compose/icons/IcPluginEopatchPreviews.kt | 2 +- .../ui/compose/icons/IcPluginEquilPreviews.kt | 2 +- .../icons/IcPluginEversensePreviews.kt | 2 +- .../ui/compose/icons/IcPluginFoodPreviews.kt | 2 +- .../compose/icons/IcPluginGarminPreviews.kt | 2 +- .../ui/compose/icons/IcPluginGlimpPreviews.kt | 2 +- .../compose/icons/IcPluginGlunovoPreviews.kt | 2 +- .../compose/icons/IcPluginInsightPreviews.kt | 2 +- .../compose/icons/IcPluginInsulinPreviews.kt | 2 +- .../icons/IcPluginIntelligoPreviews.kt | 2 +- .../icons/IcPluginMaintenancePreviews.kt | 2 +- .../icons/IcPluginMedtronicPreviews.kt | 2 +- .../compose/icons/IcPluginMedtrumPreviews.kt | 2 +- .../compose/icons/IcPluginMm640GPreviews.kt | 2 +- .../icons/IcPluginNsClientBgPreviews.kt | 2 +- .../compose/icons/IcPluginNsClientPreviews.kt | 2 +- .../icons/IcPluginObjectivesPreviews.kt | 2 +- .../compose/icons/IcPluginOmnipodPreviews.kt | 2 +- .../compose/icons/IcPluginOpenApsPreviews.kt | 2 +- .../icons/IcPluginOpenHumansPreviews.kt | 2 +- .../compose/icons/IcPluginPocTechPreviews.kt | 2 +- .../compose/icons/IcPluginRandomBgPreviews.kt | 2 +- .../ui/compose/icons/IcPluginSmsPreviews.kt | 2 +- .../ui/compose/icons/IcPluginSyaiPreviews.kt | 2 +- .../ui/compose/icons/IcPluginTMobiPreviews.kt | 2 +- .../compose/icons/IcPluginTidepoolPreviews.kt | 2 +- .../ui/compose/icons/IcPluginTizenPreviews.kt | 2 +- .../compose/icons/IcPluginTomatoPreviews.kt | 2 +- .../icons/IcPluginVirtualPumpPreviews.kt | 2 +- .../ui/compose/icons/IcProfilePreviews.kt | 2 +- .../ui/compose/icons/IcPumpBatteryPreviews.kt | 2 +- .../compose/icons/IcPumpCartridgePreviews.kt | 2 +- .../ui/compose/icons/IcQuestionPreviews.kt | 2 +- .../ui/compose/icons/IcQuickWizardPreviews.kt | 2 +- .../ui/compose/icons/IcSettingsOffPreviews.kt | 2 +- .../ui/compose/icons/IcSetupWizardPreviews.kt | 2 +- .../compose/icons/IcSiteRotationPreviews.kt | 2 +- .../core/ui/compose/icons/IcSmbPreviews.kt | 2 +- .../core/ui/compose/icons/IcStatsPreviews.kt | 2 +- .../ui/compose/icons/IcTbrCancelPreviews.kt | 2 +- .../ui/compose/icons/IcTbrHighPreviews.kt | 2 +- .../core/ui/compose/icons/IcTbrLowPreviews.kt | 2 +- .../ui/compose/icons/IcTtActivityPreviews.kt | 2 +- .../ui/compose/icons/IcTtCancelPreviews.kt | 2 +- .../compose/icons/IcTtEatingSoonPreviews.kt | 2 +- .../core/ui/compose/icons/IcTtHighPreviews.kt | 2 +- .../core/ui/compose/icons/IcTtHypoPreviews.kt | 2 +- .../ui/compose/icons/IcTtManualPreviews.kt | 2 +- .../ui/compose/icons/IcUserOptionsPreviews.kt | 2 +- .../core/ui/compose/icons/IcXDripPreviews.kt | 2 +- .../aaps/core/ui/compose/icons/NsPreviews.kt | 2 +- .../core/ui/compose/icons/PumpPreviews.kt | 2 +- .../icons/library/IcChildBackPreviews.kt | 2 +- .../icons/library/IcChildFrontPreviews.kt | 2 +- .../icons/library/IcManBackPreviews.kt | 2 +- .../icons/library/IcManFrontPreviews.kt | 2 +- .../icons/library/IcWomanBackPreviews.kt | 2 +- .../icons/library/IcWomanFrontPreviews.kt | 2 +- .../unused/IcActivityTreatmentsPreviews.kt | 2 +- .../library/unused/IcArrowCenterPreviews.kt | 2 +- .../library/unused/IcArrowFlatPreviews.kt | 2 +- .../library/unused/IcPluginActionPreviews.kt | 2 +- .../unused/IcPluginConfigBuilderPreviews.kt | 2 +- .../unused/IcPluginOverviewPreviews.kt | 2 +- .../ui/compose/pickers/WeekDaySelector.kt | 0 .../pickers/WeekDaySelectorPreviews.kt | 2 +- .../aaps/core/ui/compose/pump/BleScanStep.kt | 1 - .../ui/compose/pump/ProfileGateWizardStep.kt | 1 - .../pump/ProfileGateWizardStepPreviews.kt | 2 +- .../ui/compose/pump/PumpActivityDialog.kt | 1 - .../pump/PumpActivityDialogPreviews.kt | 2 +- .../compose/pump/PumpActivityFabPreviews.kt | 2 +- .../core/ui/compose/pump/PumpHistoryScreen.kt | 1 - .../siteRotation/ArrowSelectionDialog.kt | 1 - .../ArrowSelectionDialogPreviews.kt | 2 +- .../siteRotation/SiteLocationSummary.kt | 1 - .../SiteLocationSummaryPreviews.kt | 2 +- .../aaps/core/ui/extensions/TrendArrowIcon.kt | 0 .../aaps/core/ui/compose/FontPadding.ios.kt | 6 + .../core/ui/compose/TextRefResource.ios.kt | 25 ++ .../kotlin/app/aaps/core/utils/HtmlHelper.kt | 15 - .../kotlin/app/aaps/database/AppRepository.kt | 2 +- .../interfaces/pump/PumpEnactResultTest.kt | 15 - .../extensions/PumpEnactResultExtension.kt | 43 --- .../PumpEnactResultExtensionTest.kt | 286 ------------------ .../plugins/automation/AutomationRuntime.kt | 42 +-- .../automation/compose/AutomationScreen.kt | 15 +- .../automation/compose/AutomationState.kt | 3 +- .../compose/AutomationStateHolder.kt | 10 +- .../compose/NSClientComposeContent.kt | 13 +- .../nsclientV3/data/NSDeviceStatusHandler.kt | 21 +- .../tidepool/events/EventTidepoolStatus.kt | 14 - .../aaps/pump/insight/InsightAlertService.kt | 3 +- .../activities/InsightAlertActivity.kt | 3 +- .../insight/compose/InsightAlertScreen.kt | 4 +- .../omnipod/dash/OmnipodDashPumpPlugin.kt | 2 +- .../InsulinManagementScreen.kt | 12 +- .../compose/maintenance/MaintenanceDialogs.kt | 10 +- .../compose/overview/OverviewDataCacheImpl.kt | 2 +- 252 files changed, 514 insertions(+), 680 deletions(-) create mode 100644 core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/HtmlTextTest.kt create mode 100644 core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FontPadding.android.kt rename core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/{TextRefResource.kt => TextRefResource.android.kt} (57%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt (93%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt (92%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt (85%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsTypography.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt (99%) create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FontPadding.kt create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/HtmlText.kt rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt (85%) create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt (87%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt (85%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt (89%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt (94%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt (87%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt (90%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt (94%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt (92%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt (90%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt (91%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt (87%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt (95%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt (86%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt (99%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt (94%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt (100%) create mode 100644 core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/FontPadding.ios.kt create mode 100644 core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/TextRefResource.ios.kt delete mode 100644 core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt delete mode 100644 plugins/aps/src/main/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtension.kt delete mode 100644 plugins/aps/src/test/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtensionTest.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index a7de9de896ea..081baddeb381 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -85,6 +85,7 @@ kotlin { api(libs.cmp.ui) api(libs.cmp.material3) api(libs.cmp.material.icons.extended) + implementation(compose.components.uiToolingPreview) } } diff --git a/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/HtmlTextTest.kt b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/HtmlTextTest.kt new file mode 100644 index 000000000000..49b3803d71f0 --- /dev/null +++ b/core/ui/src/androidHostTest/kotlin/app/aaps/core/ui/compose/HtmlTextTest.kt @@ -0,0 +1,68 @@ +package app.aaps.core.ui.compose + +import androidx.compose.ui.text.font.FontWeight +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +internal class HtmlTextTest { + + @Test fun `plain text is unchanged`() { + assertThat("just text".htmlToAnnotatedString().text).isEqualTo("just text") + } + + @Test fun `br becomes a newline`() { + assertThat("a
b".htmlToAnnotatedString().text).isEqualTo("a\nb") + assertThat("a
b".htmlToAnnotatedString().text).isEqualTo("a\nb") + assertThat("a
b".htmlToAnnotatedString().text).isEqualTo("a\nb") + assertThat("a
b".htmlToAnnotatedString().text).isEqualTo("a\nb") + } + + @Test fun `b marks the enclosed text bold and nothing else`() { + val result = "noyesno".htmlToAnnotatedString() + assertThat(result.text).isEqualTo("noyesno") + val spans = result.spanStyles + assertThat(spans).hasSize(1) + assertThat(spans[0].item.fontWeight).isEqualTo(FontWeight.Bold) + assertThat(spans[0].start).isEqualTo(2) + assertThat(spans[0].end).isEqualTo(5) + } + + @Test fun `strong is treated as bold`() { + val result = "x".htmlToAnnotatedString() + assertThat(result.text).isEqualTo("x") + assertThat(result.spanStyles.single().item.fontWeight).isEqualTo(FontWeight.Bold) + } + + @Test fun `entities are decoded`() { + assertThat("a&b".htmlToAnnotatedString().text).isEqualTo("a&b") + assertThat("<tag>".htmlToAnnotatedString().text).isEqualTo("") + assertThat("say "hi"".htmlToAnnotatedString().text).isEqualTo("say \"hi\"") + assertThat("it's".htmlToAnnotatedString().text).isEqualTo("it's") + } + + @Test fun `a bare ampersand survives`() { + assertThat("a & b".htmlToAnnotatedString().text).isEqualTo("a & b") + assertThat("a ¬anentity; b".htmlToAnnotatedString().text).isEqualTo("a ¬anentity; b") + } + + @Test fun `an unknown tag is kept rather than dropped`() { + assertThat("2 < 3".htmlToAnnotatedString().text).isEqualTo("2 < 3") + assertThat("x".htmlToAnnotatedString().text).isEqualTo("x") + } + + @Test fun `an unclosed b still ends`() { + val result = "forever".htmlToAnnotatedString() + assertThat(result.text).isEqualTo("forever") + assertThat(result.spanStyles.single().end).isEqualTo(7) + } + + @Test fun `a stray closing tag is ignored`() { + assertThat("xy".htmlToAnnotatedString().text).isEqualTo("xy") + } + + @Test fun `the real pump enact result shape renders`() { + val result = "Success: false
Enacted: true".htmlToAnnotatedString() + assertThat(result.text).isEqualTo("Success: false\nEnacted: true") + assertThat(result.spanStyles).hasSize(2) + } +} diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt index c8772a590c98..b5ceb1554102 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt @@ -4,7 +4,6 @@ import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings import androidx.annotation.StringRes import app.aaps.core.interfaces.clientcontrol.FailureReason -import app.aaps.core.ui.R /** * The single localized-string mapping for a client-control [FailureReason], shared by the phone pending dialog diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt index bf3a3ca979b8..f5decd4f1fed 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R /** * Compact carb time row with inline expand/collapse. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FontPadding.android.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FontPadding.android.kt new file mode 100644 index 000000000000..849a161e9768 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FontPadding.android.kt @@ -0,0 +1,5 @@ +package app.aaps.core.ui.compose + +import androidx.compose.ui.text.PlatformTextStyle + +actual fun noFontPaddingPlatformStyle(): PlatformTextStyle? = PlatformTextStyle(includeFontPadding = false) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt index 3512fd1c366d..70499ccec22f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * Full-width error banner that explains why a screen's edit controls are disabled: the client's master diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt index cb72131c43d6..19fd06a2bd53 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R import kotlin.math.roundToInt /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt index 51d627015d28..da75fe0c56d4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt @@ -3,9 +3,8 @@ package app.aaps.core.ui.compose import app.aaps.core.ui.UiStrings import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt index 6c65834a5667..4b31af03c192 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt @@ -4,7 +4,6 @@ import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings import androidx.annotation.StringRes import app.aaps.core.data.plugin.PluginType -import app.aaps.core.ui.R /** * Single source of truth for the plugin-category → title string mapping, shared by the Configuration diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt index 982207c85743..d2af22671430 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt @@ -4,10 +4,9 @@ import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.android.kt similarity index 57% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt rename to core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.android.kt index 8c985d26754d..9462cfee069c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TextRefResource.android.kt @@ -5,22 +5,15 @@ import androidx.compose.ui.res.stringResource import app.aaps.core.interfaces.InterfacesStringIds import app.aaps.core.keys.KeysStringIds import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs import app.aaps.core.ui.UiStringIds /** - * Resolves a [TextRef] to text inside a Composable. - * - * Every preference screen funnels through this one function, which is the point: a module that - * changes how it stores its strings changes only this resolver. The ~18 call sites do not change - * again. - * - * Both resource forms end up in the platform `stringResource` on Android. [TextRef.AndroidRes] - * carries the id already; [TextRef.Named] carries the name from `strings.xml` and is turned into an - * id first, so Android keeps doing its own locale matching in both cases. + * Both resource forms end up in the platform `stringResource`. [TextRef.AndroidRes] carries the id + * already; [TextRef.Named] carries the name from `strings.xml` and is turned into an id first, so + * Android keeps doing its own locale matching in both cases. */ @Composable -fun stringResource(ref: TextRef): String = when (ref) { +actual fun stringResource(ref: TextRef): String = when (ref) { is TextRef.Literal -> ref.text is TextRef.AndroidRes -> if (ref.args.isEmpty()) stringResource(ref.id) @@ -53,15 +46,3 @@ private fun androidIdOf(ref: TextRef.Named): Int? = when (ref.owner) { "interfaces" -> InterfacesStringIds.idOf(ref.name) else -> null } - -/** - * Same, with format arguments - mirrors `androidx.compose.ui.res.stringResource(id, vararg)`, which - * is what the call sites used before they stopped naming resource ids. - */ -@Composable -fun stringResource(ref: TextRef, vararg formatArgs: Any): String = - stringResource(ref.withArgs(*formatArgs)) - -/** Same, for an optional reference - returns null so callers can keep using `?.let { }`. */ -@Composable -fun stringResourceOrNull(ref: TextRef?): String? = ref?.let { stringResource(it) } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt index 2940fe94ec62..f37cc87c5638 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R import app.aaps.core.ui.compose.dialogs.TimePickerModal /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt index 5c4ff375f5c9..98fb66429883 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt @@ -33,7 +33,6 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventShowSnackbar -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.SnackbarColors diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt index a87f3c5904d0..8b14c4afb02e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R import app.aaps.core.ui.compose.formatMinutesAsDuration import app.aaps.core.ui.compose.stringResource import kotlin.math.roundToInt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt index f81a33a7fcf2..e69452e145ac 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.interfaces.TextRef @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt index 0fed855ab0b9..5ba8b62ddab6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import app.aaps.core.interfaces.insulin.ConcentrationType -import app.aaps.core.ui.R @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt index ddd2faf8a51c..7dab760f4322 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.aaps.core.data.model.ICfg import app.aaps.core.interfaces.insulin.ConcentrationType -import app.aaps.core.ui.R /** * @see PreviewCollapsed diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt index 09bd9f2ab1fd..73c406f9b834 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.insulin import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.ICfg private val previewInsulins = listOf( diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt index 4a4308f4cf89..8033c386ac9d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.interfaces.navigation.ElementCategory import app.aaps.core.interfaces.navigation.ElementType -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.icons.IcActivity import app.aaps.core.ui.compose.icons.IcAnnouncement diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt index 6814955e3974..f24ee7ad5daf 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt @@ -17,7 +17,6 @@ import app.aaps.core.keys.interfaces.DoublePreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.keys.step -import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.isDuration import app.aaps.core.ui.compose.rangeText diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt index 85923ac97f9a..2bccd3ab8252 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.DoubleKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt index 96bd22ec838c..3b6848032ed3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt @@ -15,7 +15,6 @@ import app.aaps.core.data.format.NumberFormat import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.ui.R import app.aaps.core.ui.compose.isDuration import app.aaps.core.ui.compose.rangeText import app.aaps.core.ui.compose.stringResource diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt index 66e0dc073a8d..12f9d6c477d5 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.IntKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt index dec87fb1df9a..438ca414e43f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.IntKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt index 9d5cb3e23d2c..e303b6b8b900 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import app.aaps.core.keys.StringKey import app.aaps.core.ui.compose.stringResource -import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.dialogs.QueryPasswordDialog import app.aaps.core.ui.compose.dialogs.SetPasswordDialog diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt index 88c08a3a87b1..e1f855a9844c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt index 95f66d54ec30..1dedfe951d56 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt @@ -15,7 +15,6 @@ import app.aaps.core.keys.interfaces.IntPreferenceKey import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.dialogs.SetPasswordDialog import app.aaps.core.ui.compose.stringResource diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt index ba558c68d074..03a530b4b0b1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.StringKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt index 1e3a9ee103ec..a7907183fd1b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt @@ -17,7 +17,6 @@ import app.aaps.core.keys.interfaces.StringPreferenceKey import app.aaps.core.keys.interfaces.StringValidator import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.ui.R import app.aaps.core.ui.compose.stringResource /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt index 5f2546aefb1f..aa11fddf988d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.StringKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt index 87e83fc4f393..ce7017e0b68a 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt @@ -15,7 +15,6 @@ import app.aaps.core.keys.interfaces.BooleanKeyWithChangeGuard import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.ui.R import app.aaps.core.ui.compose.dialogs.OkDialog import app.aaps.core.ui.compose.stringResource diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt index feb42c7d46cf..8036d5e30c93 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.keys.BooleanKey @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt index b06a6dd1f632..7f5d5695b7ca 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt @@ -5,10 +5,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt index 825619452f80..38cac7a5bffb 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt index e14710b89410..0f43e6532c3d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt index 56e392548fee..3b4a360de44c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt index bf3ba176f488..235ccac423cd 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt index 57f6040535f4..34d242c61dd6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.unit.em import app.aaps.core.keys.interfaces.NonPreferenceKey import app.aaps.core.keys.interfaces.SyncDirection import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R import app.aaps.core.ui.compose.LocalConfig import app.aaps.core.ui.compose.stringResource diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt index 38d466db8541..6064e39a6643 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.preference import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt index 4cc57ddca2c9..376d1812d21b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BlePreCheckHost.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import app.aaps.core.interfaces.pump.BlePreCheck import app.aaps.core.interfaces.pump.BlePreCheckResult -import app.aaps.core.ui.R import app.aaps.core.ui.compose.dialogs.OkDialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt index a425d581bb27..8b252ad42328 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.Translator -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.AapsTheme import app.aaps.core.ui.compose.icons.IcCannulaChange diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt index 315f32a2d92a..c4c8fb217002 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.TE import app.aaps.core.ui.compose.icons.IcCannulaChange import app.aaps.core.ui.compose.icons.IcCgmInsert diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt index fc9002e7ed5d..bc0bdb15e2d8 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.LocalDateUtil import app.aaps.core.ui.compose.icons.IcCannulaChange diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt index 26aefaf07590..e58d46fef653 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.TE @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt index 8db2b73bb7e4..f4d17089a02c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import app.aaps.core.data.model.TE -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsTopAppBar /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt index dbc71ac43d1c..72e6b0eb65ba 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.TE @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt index 254315a7290b..d1b8733f80b4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.data.model.TE -import app.aaps.core.ui.R import app.aaps.core.ui.compose.pump.WizardButton import app.aaps.core.ui.compose.pump.WizardStepLayout import kotlinx.coroutines.flow.StateFlow diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt index 5d4d0e1ac1e5..e9e6b49e2ab5 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.TE import kotlinx.coroutines.flow.MutableStateFlow diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt index 7e7e71b61f68..d4b25b7008ec 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt @@ -110,7 +110,9 @@ sealed class SearchableItem { val pluginRef: PluginBase ) : SearchableItem() { - override val key: String = pluginRef.javaClass.simpleName + // pluginId is the documented stable identity for a plugin and defaults to the class simple name, + // so this is the same string javaClass.simpleName gave - without tying the file to the JVM. + override val key: String = pluginRef.pluginId override val title: TextRef = pluginRef.pluginDescription.pluginName ?: TextRef.Literal(pluginRef.pluginId) override val summary: TextRef? = pluginRef.pluginDescription.description override val plugin: PluginBase = pluginRef diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt similarity index 93% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt index a91e1b8bcd5f..af788da08003 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsCardPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt similarity index 92% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt index 4becbeccc4ab..145fb905b7da 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsFabPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt index 4084d66abf71..ac817fe56e38 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchField.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * Rounded search field styled like Google Contacts search bar. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt similarity index 85% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt index 58712c3208fa..f3176c933a07 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsSearchFieldPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt index bfd1565b7000..a60b1c85e89c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTopAppBarPreviews.kt @@ -10,7 +10,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true, name = "Light") @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt index 5c60493b8597..6c934638bfea 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTypography.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember -import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineHeightStyle @@ -100,7 +99,7 @@ fun aapsTypography(scale: Float = 1f): AapsTypography { */ fun Typography.withoutFontPadding(): Typography { fun TextStyle.noPad() = copy( - platformStyle = PlatformTextStyle(includeFontPadding = false), + platformStyle = noFontPaddingPlatformStyle(), lineHeightStyle = LineHeightStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt index 69acb1fdf73c..ffd2ca2e4bac 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/ConfigPluginCard.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * How a plugin category lets the user choose plugins. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt index 2760b3fc6dc0..869d5db02625 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/DateTimeSection.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * Shared date/time picker row with two read-only OutlinedTextFields. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt index 79b3a872e24b..daf9b1fbf55d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/EventTimeRow.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * Compact event time row with inline expand/collapse. diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FontPadding.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FontPadding.kt new file mode 100644 index 000000000000..abaab2bc0931 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FontPadding.kt @@ -0,0 +1,13 @@ +package app.aaps.core.ui.compose + +import androidx.compose.ui.text.PlatformTextStyle + +/** + * The platform text style that turns off the legacy font padding, or null where there is none. + * + * `includeFontPadding` is an Android text-layout quirk: the platform reserves extra space above the + * glyphs from the font ascent, which stops text from centering in its line box. No other platform + * has it, and `PlatformTextStyle` only accepts the flag on Android, so this is the one line of + * [Typography.withoutFontPadding] that cannot be shared. + */ +expect fun noFontPaddingPlatformStyle(): PlatformTextStyle? diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/HtmlText.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/HtmlText.kt new file mode 100644 index 000000000000..033a6d5499ea --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/HtmlText.kt @@ -0,0 +1,80 @@ +package app.aaps.core.ui.compose + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight + +private val BoldStyle = SpanStyle(fontWeight = FontWeight.Bold) + +/** + * Turns the small amount of HTML the app uses into styled text. + * + * Only `` and `
` appear anywhere - 36 and 4 times across every English string resource, and + * nothing else - so this handles exactly that plus the usual character entities. It replaces + * `AnnotatedString.fromHtml`, which is an Android only API and was the last thing keeping the + * dialogs off commonMain. + * + * Anything it does not recognise is kept as written rather than dropped. A `<` in user text - a note + * or a pump name - therefore survives instead of being swallowed as a broken tag, which is a small + * improvement on the platform parser. + */ +fun String.htmlToAnnotatedString(): AnnotatedString { + val builder = AnnotatedString.Builder() + var openBold = 0 + var i = 0 + while (i < length) { + when (this[i]) { + '<' -> { + val close = indexOf('>', i) + val tag = if (close > i) substring(i + 1, close).trim().removeSuffix("/").trim().lowercase() else null + when (tag) { + "b", "strong" -> { + builder.pushStyle(BoldStyle); openBold++ + } + + "/b", "/strong" -> if (openBold > 0) { + builder.pop(); openBold-- + } + + "br" -> builder.append('\n') + // Not a tag this app produces. Keep the characters so nothing is silently lost. + else -> { + builder.append(this[i]); i++; continue + } + } + i = close + 1 + } + + '&' -> { + val semi = indexOf(';', i) + val entity = if (semi in (i + 1)..(i + MaxEntityLength)) substring(i + 1, semi) else null + val decoded = entity?.let { decodeEntity(it) } + if (decoded == null) { + builder.append(this[i]); i++ + } else { + builder.append(decoded); i = semi + 1 + } + } + + else -> { + builder.append(this[i]); i++ + } + } + } + // A resource with an unbalanced would otherwise leave the style open forever. + repeat(openBold) { builder.pop() } + return builder.toAnnotatedString() +} + +/** Longest entity name handled below, so a stray `&` in text is not scanned to the end of the string. */ +private const val MaxEntityLength = 6 + +private fun decodeEntity(name: String): String? = when (name.lowercase()) { + "amp" -> "&" + "lt" -> "<" + "gt" -> ">" + "quot" -> "\"" + "apos", "#39" -> "'" + "nbsp", "#160" -> " " + else -> null +} diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt index 206e6fd71d4b..03a78fbf11e4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/InsulinSelector.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import app.aaps.core.data.model.ICfg -import app.aaps.core.ui.R /** * Drop-down for picking one insulin configuration out of the catalogue. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt similarity index 85% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt index 35d3c712122b..d24d87fae583 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/QuickAddButtonsPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt new file mode 100644 index 000000000000..dca883827c83 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TextRefResource.kt @@ -0,0 +1,30 @@ +package app.aaps.core.ui.compose + +import androidx.compose.runtime.Composable +import app.aaps.core.keys.interfaces.TextRef +import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs + +/** + * Resolves a [TextRef] to text inside a Composable. + * + * Every screen funnels through this one function, which is the point: a module that changes how it + * stores its strings changes only this resolver, and the call sites do not change again. + * + * It is `expect` because finding the text is the one part that cannot be shared. Android looks the + * reference up through `Resources`, so it keeps doing its own locale matching exactly as before. + * Every other target needs its own answer. + */ +@Composable +expect fun stringResource(ref: TextRef): String + +/** + * Same, with format arguments - mirrors `androidx.compose.ui.res.stringResource(id, vararg)`, which + * is what the call sites used before they stopped naming resource ids. + */ +@Composable +fun stringResource(ref: TextRef, vararg formatArgs: Any): String = + stringResource(ref.withArgs(*formatArgs)) + +/** Same, for an optional reference - returns null so callers can keep using `?.let { }`. */ +@Composable +fun stringResourceOrNull(ref: TextRef?): String? = ref?.let { stringResource(it) } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt index 2335d199ccf1..a14005e5ae50 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/UnitTypeText.kt @@ -4,7 +4,6 @@ import app.aaps.core.keys.interfaces.TextRef.Companion.withArgs import app.aaps.core.ui.UiStrings import app.aaps.core.keys.UnitType import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R /** * Maps a [UnitType] to the text that describes it. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt similarity index 87% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt index 1fdcf8f2851c..2dc4a610907f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/banner/BannerPreviews.kt @@ -1,7 +1,7 @@ package app.aaps.core.ui.compose.banner import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt index b3472cc09c5d..65640a798bad 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModal.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable -import app.aaps.core.ui.R /** * A modal date picker dialog. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt similarity index 85% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt index 2deda8cba4c4..eb867f2c52f2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/DatePickerModalPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt index 317ef0230067..45d585749baf 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialog.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.htmlToAnnotatedString import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons @@ -13,10 +14,8 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * An error/warning dialog with a warning icon, dismiss button, and optional positive button. @@ -55,7 +54,7 @@ fun ErrorDialog( }, text = { Text( - text = AnnotatedString.fromHtml(message.replace("\n", "
")), + text = message.htmlToAnnotatedString(), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt similarity index 89% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt index adbb825f5cf3..39b1843c6781 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ErrorDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt index d474c8a41fcb..3efaebc71a23 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialog.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.htmlToAnnotatedString import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -18,11 +19,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * A confirmation dialog with OK and Cancel buttons. @@ -74,7 +73,7 @@ fun OkCancelDialog( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = AnnotatedString.fromHtml(message.replace("\n", "
")), + text = message.htmlToAnnotatedString(), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt index b2eb79e46f03..2284ec0a16a6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkCancelDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt similarity index 94% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt index 9c6259fd6553..aa5384e44b5d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialog.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.htmlToAnnotatedString import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog @@ -11,10 +12,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * A simple alert dialog with a title, message, and OK button. @@ -44,7 +43,7 @@ fun OkDialog( ) }, text = { - Text(text = AnnotatedString.fromHtml(message.replace("\n", "
"))) + Text(text = message.htmlToAnnotatedString()) }, confirmButton = { TextButton(onClick = onDismiss) { diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt similarity index 87% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt index 877dd3ec5ecc..3f60504aa6c3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/OkDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt index 067ec42c98a7..69e805b5361e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialog.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * Dialog for querying a free-form password with optional explanation and warning messages. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt index ecc7c39fe9e0..97bef199a99d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryAnyPasswordDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt index 7e799c196dc3..5ac86d07ce58 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialog.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * Dialog for querying an existing password or PIN. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt index 61160c7caec3..a60bd98bca76 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/QueryPasswordDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt index 5d059c6f5624..228016d33155 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialog.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * Dialog for setting a new password or PIN. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt index e7900cecb516..73f4bc1bc30a 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/SetPasswordDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt index d9b3a70e2246..b75d737add64 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialog.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.htmlToAnnotatedString import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -21,11 +22,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * A confirmation dialog with three stacked, full-width actions: primary, secondary, cancel. @@ -95,7 +94,7 @@ fun ThreeButtonDialog( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = AnnotatedString.fromHtml(message.replace("\n", "
")), + text = message.htmlToAnnotatedString(), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt similarity index 90% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt index 05db54607c0a..134dbf00c30e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ThreeButtonDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt index fbe44dab2b25..f964490d9004 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModal.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.TimePicker import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * A modal time picker dialog. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt index f868519cb05c..6e739f98fbb9 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/TimePickerModalPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt index adf9abfbeca3..4dc55a3ec70e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/UnifiedAuthDialog.kt @@ -32,7 +32,6 @@ import app.aaps.core.interfaces.protection.AuthorizationResult import app.aaps.core.interfaces.protection.ProtectionCheck import app.aaps.core.interfaces.protection.ProtectionResult import app.aaps.core.interfaces.protection.ProtectionType -import app.aaps.core.ui.R /** * Unified authentication dialog that accepts a single credential input and tries it diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt index 3861a0ead6b2..ba3ab2395483 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialog.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResource +import app.aaps.core.ui.compose.htmlToAnnotatedString import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -10,10 +11,8 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties -import app.aaps.core.ui.R /** * A dialog with Yes, No, and Cancel buttons. @@ -44,7 +43,7 @@ fun YesNoCancelDialog( ) }, text = { - Text(text = AnnotatedString.fromHtml(message.replace("\n", "
"))) + Text(text = message.htmlToAnnotatedString()) }, confirmButton = { TextButton(onClick = onYes) { diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt index 35f0e66abf51..a143ad4a1a97 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/YesNoCancelDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.dialogs import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt index 4ec00d3e04a9..219d2ecd2d85 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/CareportalPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt index 80b3f0b484cc..a9e329813a48 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAapsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt index f09346501df1..577d2eb3f8d6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActionPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt index 9cbb6b3245ac..a35341efc169 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcActivityPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt index bf033bb6523c..43c97e1d9039 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAnnouncementPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt index 228552dfdc7e..9fa1aa0ada0d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleDownPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt index e793199493f2..74afbf516f19 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowDoubleUpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt index f54bf326df30..48073dbc1c16 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFlatPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt index 39c59cee1688..99e1b5420748 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveDownPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt index bc22afe89d36..232ea4c9075c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowFortyFiveUpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt index 344739ad0dcd..e29a20d4c2b1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowInvalidPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt index cf3709188c29..4b437341026f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftDownPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt index 3402c5e2f9be..2dc6409cd36b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt index 1a6e62401e9a..a22b22c17023 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowLeftUpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt index 9484d2b43d4d..569cb2ef1581 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowNonePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt index d713967cab56..bef88ed17092 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleDownPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt index dcbcc3d8d196..9eecdf904225 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcArrowSimpleUpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt index e5af55a16b58..4f206efddef7 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAbovePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt index 72d92f090d00..9e6646eb0d35 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsAboveXPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt index 509200aac522..a85a548ff11b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt index 56f5c86d65d8..f6a4031bc6fa 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsBelowXPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt index 8e1bc090e171..a449c6677ea6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt index cef5210d82a0..a5ad8d2eb83b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAsXPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt index 4c24763d534b..0efb55a8d8ff 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcAutomationPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt index 28140f237cfb..cbcd536e169d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBgCheckPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt index ef0c511fc952..40c1077dfe39 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBolusPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt index 41fdc6a72744..67cdb46cc7b3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcBreadPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt index f25987b1fa5a..19d23212a988 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcByodaPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt index 00a9421bc143..8c8e4347857e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCakePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt index 599417a4cecf..38c396115558 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalculatorPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt index 5569b9b5ec43..99710899802b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCalibrationPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt index bdf38340f8f9..a224785b7315 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCancelExtendedBolusPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt index 2024986b9406..ffcc7c8356d4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCannulaChangePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt index 07dcded282be..0947f4aadddb 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCarbsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt index 79cfbc262fae..9480ef2a8c5e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCgmInsertPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt index 3334d21bbf6b..04240634d791 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcClinicalNotesPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt index 5e5b68277c7f..15a9ea1cd256 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcCompareProfilesPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt similarity index 94% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt index c4aed5c05d76..8c666b822bdc 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDeltaPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt index 8d0e588bbeef..7f2e7f0a6739 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcDiaconnPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt index 71491144132f..ed910a0b8921 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcExtendedBolusPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt index 640c415aa412..656226596769 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericCgmPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt index 7225fbf64354..9f9a126bf371 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGenericIconPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt index 15baed9c29f7..964687dda156 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcGoogleDrivePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt index f7c59cb73035..1888f4370bb1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcHistoryPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt index a47868a9faff..05b0cfb342ce 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopClosedPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt index 3711280c5850..3a2fe85d25f2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisabledPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt index 019b48142c2c..b052e7ac3033 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopDisconnectedPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt index 737c8b1a87c2..c6eb67993dfa 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopHiddenPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt index e1c0ada090f3..58c1a04e41f9 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopLgsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt index 7e02f33b9cf4..da6b2cc2a879 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopOpenPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt index dca5ca42df67..2759bf3faa31 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedDstPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt index 2320999f0ed6..74db153024b6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt index 779a47503078..8a45c9299620 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopPausedPumpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt index 0e0e6b7000d4..a5542c00c66d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopReconnectPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt index 4361f2035ffe..50e9c8fc958c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcLoopSuperBolusPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt index 4b28d0e966b0..d663b8e3535c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcMdiPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt index 8b66c2269d98..fb559f42a329 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNoTbrPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt index b5389890083b..c2e60d601326 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcNotePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt index 34b48386417f..33bcd73d202e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPatchPumpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt index 094b1e8bca35..e59add678f28 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPizzaPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt index 574913dcc073..5796083c11c9 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutomationPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt index d193e9e59c03..bf07a7494f43 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginAutotunePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt index 0b60ad95089d..afacfc773626 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginByodaPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt index 839b220fa12a..ad438a170bc2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginComboPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt index 8aaef9d91288..e01772dc1be3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginConfigBuilderPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt index 7b2cce62c95d..00b23c1315cc 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDanaPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt index 3518bca99c7b..6ab2195c23a3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginDiaconnPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt index 127aea6f3ff3..af4eadbe4f34 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEopatchPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt index 82562d1b4418..bb7c4debecd4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEquilPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt index 68477602d0bb..c1c583f1d9bd 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginEversensePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt index cb55b9cb7971..b193a0965a32 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginFoodPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt index 5d87addfc00e..8378399b32d4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGarminPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt index a52195cfc860..b8bda690af12 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlimpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt index 7db5576c13d5..01a4cd0d5d49 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginGlunovoPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt index cc8b9bd9d298..a7004fa8aa02 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsightPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt index 6729df4626a9..67e5112342f4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginInsulinPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt index 5d8d0cb3ebd2..2ff2364ba0cb 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginIntelligoPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt index 780186fd6b6f..de8cabcb34f8 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMaintenancePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt index 9468f83785f5..9ae14a1ebc4d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtronicPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt index 3678142fca82..c01d4eae5fa1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMedtrumPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt index f7fdbfd350ee..d5ed3eb198de 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginMm640GPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt index 17fc6f8472c4..49f7dfd6a0b1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientBgPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt index eeb0c3532905..a537dd4c938e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginNsClientPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt index f95fad5d2a9c..48a195bd24f4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginObjectivesPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt index 84f7834e6817..beb446a78781 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOmnipodPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt index e4d6c7bd3d7f..0e44d78dfc28 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenApsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt index 240e6d3c00e0..39183c427643 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginOpenHumansPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt index 14b3969557c7..58784a41cc42 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginPocTechPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt index 41d0cf48f31a..dff7d729119f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginRandomBgPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt index 89d1ddcac6ac..cb74155976e6 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSmsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt index 8fa7017eecc8..01db63dbe3da 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginSyaiPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt index c3c7565c7d72..a28a89bd6330 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTMobiPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt index 9a381908b7b1..d23d74dc3686 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTidepoolPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt index d6a9ec484c01..c65ca695b9ba 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTizenPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt index f046ff28813b..ee72c3644483 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginTomatoPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt index 1bdc2b60d199..ff384f6f35ca 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPluginVirtualPumpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt index 654a7dd1dfbc..4f29f37548ae 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcProfilePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt index 8f60d1eee082..3205cf4d6fa0 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpBatteryPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt index 3a56f2c5b8a4..fb6ea298dcce 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcPumpCartridgePreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt index 695fa676ac43..46946ed5b0d2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuestionPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt index 76ed94f93f79..4468c543dd7c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcQuickWizardPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt similarity index 92% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt index 087f9367c2da..3d396688a00a 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSettingsOffPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt similarity index 90% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt index 7a6975a6f7bf..4ba4b643292d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSetupWizardPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt index 7d923d53af62..5d9b54e1b09e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSiteRotationPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt index 0a66e8e22047..ba8fa58832d4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcSmbPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt index 1131d39119cc..bed2067c02cc 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcStatsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt index 97b0d5ad7e0f..c71b7ba7840c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrCancelPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt index ef2b9199e126..3108f3cebc0b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrHighPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt index e8e8ca04f36c..13f4428614ca 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTbrLowPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt index ca2a13abcb59..61971c45d1e8 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtActivityPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt index 844ff122a4db..332c52de8906 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtCancelPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt index 23a5b34e69d5..b16fe2ab7ef4 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtEatingSoonPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt index 505da94e8142..77a944cd1fb1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHighPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt index 1af1df7ec6c8..5370b7d12d51 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtHypoPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt index e09840e45f0e..00618380bc73 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcTtManualPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt index 862f89ef67a3..d09ae0eca4e2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcUserOptionsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt index 5851a743a692..ed0b8bf2171c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/IcXDripPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt index 1d6f41e4fe4f..51c97cfb8d82 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/NsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt similarity index 91% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt index 665e8ae4d053..57ebb00de565 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/PumpPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt index 9d0419bc5b75..00419c6c80a3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildBackPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt index 0fd493cf7382..27c30b1dfa2f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcChildFrontPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt index 9d38eab2a4e5..e3a9c357e5af 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManBackPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt index 64d464924905..7bb92ff341ee 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcManFrontPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt index 9123d819ecff..8e9207a1ab1c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanBackPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt index 26dd5255fe95..f35f8825c88b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/IcWomanFrontPreviews.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt index 2a745f98d751..8e09b41a28ee 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcActivityTreatmentsPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt index c8f1909b2fd9..7a9905a65d88 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowCenterPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt index c56a8f3a9532..10a2063db22f 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcArrowFlatPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt index 50b721b85414..5f3d43748b4e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginActionPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt index 1c22435fa7c8..b0c7d0db635d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginConfigBuilderPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt index 0cb3ae8d2fb1..88e76c0b82f1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/icons/library/unused/IcPluginOverviewPreviews.kt @@ -6,7 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt similarity index 87% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt index 13ac139b9ef7..60c5372a06d2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelectorPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.pickers import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt index 313424ca03a7..5618034e26aa 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/BleScanStep.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.aaps.core.interfaces.pump.ble.ScannedDevice -import app.aaps.core.ui.R /** * Shared BLE device scan step for pump pairing wizards. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt index 4bcf508ec23b..a3fe18ed8a23 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStep.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing import kotlinx.coroutines.flow.StateFlow diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt index 28eb425fa0c8..9fd3908cb67c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/ProfileGateWizardStepPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.pump import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import kotlinx.coroutines.flow.MutableStateFlow @Preview(showBackground = true, name = "ProfileGate - has profiles") diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt index 49031a70746b..1ac3671cf5e2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialog.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import app.aaps.core.interfaces.pump.BolusProgressState -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt index 329ce2d4c9e0..e88632c00946 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityDialogPreviews.kt @@ -3,7 +3,7 @@ package app.aaps.core.ui.compose.pump import androidx.compose.ui.text.AnnotatedString import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.keys.interfaces.TextRef diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt similarity index 95% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt index f3574de41fae..33e95a44112c 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpActivityFabPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.pump import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.interfaces.pump.BolusProgressState import app.aaps.core.interfaces.pump.PumpInsulin import app.aaps.core.keys.interfaces.TextRef diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt index 4df0b4fb3cd5..83b77cdaa7c3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpHistoryScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import app.aaps.core.ui.R /** * Shared pump history screen scaffold with type dropdown, reload button, and record list. diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt index b9244d0a6f5a..dc6a5cb239b3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialog.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE -import app.aaps.core.ui.R /** * @see ArrowSelectionDialogPreview diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt similarity index 86% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt index e5bb8f0d185f..e27bfe7daf61 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/ArrowSelectionDialogPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview @Preview(showBackground = true) @Composable diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt similarity index 99% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt index b1016d035b6f..0b8749731520 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummary.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.aaps.core.data.model.TE -import app.aaps.core.ui.R import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.icons.IcCannulaChange import app.aaps.core.ui.compose.icons.IcCgmInsert diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt similarity index 94% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt index cd296e79a627..f459d4bf47bf 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationSummaryPreviews.kt @@ -2,7 +2,7 @@ package app.aaps.core.ui.compose.siteRotation import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import app.aaps.core.data.model.TE @Preview(showBackground = true) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TrendArrowIcon.kt diff --git a/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/FontPadding.ios.kt b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/FontPadding.ios.kt new file mode 100644 index 000000000000..6872a3567e4a --- /dev/null +++ b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/FontPadding.ios.kt @@ -0,0 +1,6 @@ +package app.aaps.core.ui.compose + +import androidx.compose.ui.text.PlatformTextStyle + +/** iOS text layout has no legacy font padding, so there is nothing to switch off. */ +actual fun noFontPaddingPlatformStyle(): PlatformTextStyle? = null diff --git a/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/TextRefResource.ios.kt b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/TextRefResource.ios.kt new file mode 100644 index 000000000000..91c1834346db --- /dev/null +++ b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/TextRefResource.ios.kt @@ -0,0 +1,25 @@ +package app.aaps.core.ui.compose + +import androidx.compose.runtime.Composable +import app.aaps.core.keys.interfaces.TextRef + +/** + * PLACEHOLDER. iOS has no string table yet, so this returns something readable rather than the real + * translation. + * + * It exists because `expect` needs an `actual` on every target, and without an iOS one the module + * would not compile for iOS at all - which is the check that keeps commonMain honest. Nothing on + * iOS runs yet, so nothing shows these strings to anyone. + * + * When iOS gains a string table, this is the only place that has to change: + * - [TextRef.Named] already carries the name from `strings.xml` and the module that owns it, which + * is exactly what a `.strings` lookup needs. That is the form to aim for. + * - [TextRef.AndroidRes] carries a number that means nothing off Android. Files still using it have + * to move to [TextRef.Named] first, so it deliberately has no sensible answer here. + */ +@Composable +actual fun stringResource(ref: TextRef): String = when (ref) { + is TextRef.Literal -> ref.text + is TextRef.Named -> ref.name + is TextRef.AndroidRes -> "?" +} diff --git a/core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt b/core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt deleted file mode 100644 index 87c8b2884992..000000000000 --- a/core/utils/src/androidMain/kotlin/app/aaps/core/utils/HtmlHelper.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.aaps.core.utils - -import android.text.Html -import android.text.SpannableStringBuilder -import android.text.Spanned - -object HtmlHelper { - - fun fromHtml(source: String): Spanned = - try { - Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY) - } catch (_: Exception) { - SpannableStringBuilder("") - } -} \ No newline at end of file diff --git a/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt b/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt index 31ae262a88af..6a26d6d3da83 100644 --- a/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt +++ b/database/impl/src/main/kotlin/app/aaps/database/AppRepository.kt @@ -224,7 +224,7 @@ class AppRepository @Inject internal constructor( val ret = StringBuilder() removed .filter { it.second > 0 } - .forEach { ret.append(it.first + " " + it.second + "
") } + .forEach { ret.appendLine(it.first + " " + it.second) } // VACUUM is intentionally NOT run here. It is memory heavy and crashed (SQLITE_NOMEM) when // it overlapped live DB activity; defragmenting VACUUM now runs only at startup while the // DB is quiescent (see vacuumDatabase / MainApp.vacuumDatabaseIfDue). diff --git a/implementation/src/test/kotlin/app/aaps/interfaces/pump/PumpEnactResultTest.kt b/implementation/src/test/kotlin/app/aaps/interfaces/pump/PumpEnactResultTest.kt index 7746f938c78d..62e4b9d661d6 100644 --- a/implementation/src/test/kotlin/app/aaps/interfaces/pump/PumpEnactResultTest.kt +++ b/implementation/src/test/kotlin/app/aaps/interfaces/pump/PumpEnactResultTest.kt @@ -2,7 +2,6 @@ package app.aaps.interfaces.pump import app.aaps.core.interfaces.pump.PumpEnactResult import app.aaps.implementation.pump.PumpEnactResultObject -import app.aaps.plugins.aps.extensions.toHtml import app.aaps.plugins.aps.loop.extensions.json import app.aaps.pump.virtual.extensions.toText import app.aaps.shared.tests.TestBaseWithProfile @@ -147,20 +146,6 @@ class PumpEnactResultTest : TestBaseWithProfile() { ) } - @Test fun toHtmlTest() { - - var per: PumpEnactResult = PumpEnactResultObject(rh).enacted(true).bolusDelivered(10.0).comment("AAA") - assertThat(per.toHtml(rh, decimalFormatter)).isEqualTo("Success: false
Enacted: true
Comment: AAA
SMB: 10.0 U") - per = PumpEnactResultObject(rh).enacted(true).isTempCancel(true).comment("AAA") - assertThat(per.toHtml(rh, decimalFormatter)).isEqualTo("Success: false
Enacted: true
Comment: AAA
Cancel temp basal") - per = PumpEnactResultObject(rh).enacted(true).isPercent(true).percent(90).duration(20).comment("AAA") - assertThat(per.toHtml(rh, decimalFormatter)).isEqualTo("Success: false
Enacted: true
Comment: AAA
Duration: 20 min
Percent: 90%") - per = PumpEnactResultObject(rh).enacted(true).isPercent(false).absolute(1.0).duration(30).comment("AAA") - assertThat(per.toHtml(rh, decimalFormatter)).isEqualTo("Success: false
Enacted: true
Comment: AAA
Duration: 30 min
Absolute: 1.00 U/h") - per = PumpEnactResultObject(rh).enacted(false).comment("AAA") - assertThat(per.toHtml(rh, decimalFormatter)).isEqualTo("Success: false
Comment: AAA") - } - @Test fun jsonTest() { var o: JSONObject? diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtension.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtension.kt deleted file mode 100644 index 7bf7945884b5..000000000000 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtension.kt +++ /dev/null @@ -1,43 +0,0 @@ -package app.aaps.plugins.aps.extensions - -import app.aaps.core.interfaces.pump.PumpEnactResult -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DecimalFormatter - -fun PumpEnactResult.toHtml(rh: ResourceHelper, decimalFormatter: DecimalFormatter): String { - var ret = "" + rh.gs(app.aaps.core.ui.R.string.success) + ": " + success - if (queued) { - ret = rh.gs(app.aaps.core.ui.R.string.waitingforpumpresult) - } else if (enacted) { - when { - bolusDelivered > 0 -> { - ret += "
" + rh.gs(app.aaps.core.ui.R.string.enacted) + ": " + enacted - if (comment.isNotEmpty()) ret += "
" + rh.gs(app.aaps.core.ui.R.string.comment) + ": " + comment - ret += "
" + rh.gs(app.aaps.core.ui.R.string.smb_shortname) + ": " + bolusDelivered + " " + rh.gs(app.aaps.core.ui.R.string.insulin_unit_shortname) - } - - isTempCancel -> { - ret += "
" + rh.gs(app.aaps.core.ui.R.string.enacted) + ": " + enacted - ret += "
" + rh.gs(app.aaps.core.ui.R.string.comment) + ": " + comment + - "
" + rh.gs(app.aaps.core.ui.R.string.cancel_temp) - } - - isPercent && percent != -1 -> { - ret += "
" + rh.gs(app.aaps.core.ui.R.string.enacted) + ": " + enacted - if (comment.isNotEmpty()) ret += "
" + rh.gs(app.aaps.core.ui.R.string.comment) + ": " + comment - ret += "
" + rh.gs(app.aaps.core.ui.R.string.duration) + ": " + duration + " min" - ret += "
" + rh.gs(app.aaps.core.ui.R.string.percent) + ": " + percent + "%" - } - - absolute != -1.0 -> { - ret += "
" + rh.gs(app.aaps.core.ui.R.string.enacted) + ": " + enacted - if (comment.isNotEmpty()) ret += "
" + rh.gs(app.aaps.core.ui.R.string.comment) + ": " + comment - ret += "
" + rh.gs(app.aaps.core.ui.R.string.duration) + ": " + duration + " min" - ret += "
" + rh.gs(app.aaps.core.ui.R.string.absolute) + ": " + decimalFormatter.to2Decimal(absolute) + " U/h" - } - } - } else { - if (comment.isNotEmpty()) ret += "
" + rh.gs(app.aaps.core.ui.R.string.comment) + ": " + comment - } - return ret -} \ No newline at end of file diff --git a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtensionTest.kt b/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtensionTest.kt deleted file mode 100644 index 5bdcf2f533d2..000000000000 --- a/plugins/aps/src/test/kotlin/app/aaps/plugins/aps/extensions/PumpEnactResultExtensionTest.kt +++ /dev/null @@ -1,286 +0,0 @@ -package app.aaps.plugins.aps.extensions - -import app.aaps.core.interfaces.pump.PumpEnactResult -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.shared.tests.TestBase -import com.google.common.truth.Truth.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.Mock -import org.mockito.kotlin.any -import org.mockito.kotlin.whenever - -class PumpEnactResultExtensionTest : TestBase() { - - @Mock lateinit var rh: ResourceHelper - @Mock lateinit var decimalFormatter: DecimalFormatter - - // Simple test implementation of PumpEnactResult - private class TestPumpEnactResult : PumpEnactResult { - override var success = false - override var enacted = false - override var comment = "" - override var duration = -1 - override var absolute = -1.0 - override var percent = -1 - override var isPercent = false - override var isTempCancel = false - override var bolusDelivered = 0.0 - override var queued = false - - override fun success(success: Boolean) = apply { this.success = success } - override fun enacted(enacted: Boolean) = apply { this.enacted = enacted } - override fun comment(comment: String) = apply { this.comment = comment } - override fun comment(comment: Int) = apply { this.comment = comment.toString() } - override fun duration(duration: Int) = apply { this.duration = duration } - override fun absolute(absolute: Double) = apply { this.absolute = absolute } - override fun percent(percent: Int) = apply { this.percent = percent } - override fun isPercent(isPercent: Boolean) = apply { this.isPercent = isPercent } - override fun isTempCancel(isTempCancel: Boolean) = apply { this.isTempCancel = isTempCancel } - override fun bolusDelivered(bolusDelivered: Double) = apply { this.bolusDelivered = bolusDelivered } - override fun queued(queued: Boolean) = apply { this.queued = queued } - } - - @BeforeEach - fun setup() { - // Setup common resource string mocks - whenever(rh.gs(app.aaps.core.ui.R.string.success)).thenReturn("Success") - whenever(rh.gs(app.aaps.core.ui.R.string.enacted)).thenReturn("Enacted") - whenever(rh.gs(app.aaps.core.ui.R.string.comment)).thenReturn("Comment") - whenever(rh.gs(app.aaps.core.ui.R.string.duration)).thenReturn("Duration") - whenever(rh.gs(app.aaps.core.ui.R.string.percent)).thenReturn("Percent") - whenever(rh.gs(app.aaps.core.ui.R.string.absolute)).thenReturn("Absolute") - whenever(rh.gs(app.aaps.core.ui.R.string.waitingforpumpresult)).thenReturn("Waiting for pump result") - whenever(rh.gs(app.aaps.core.ui.R.string.smb_shortname)).thenReturn("SMB") - whenever(rh.gs(app.aaps.core.ui.R.string.insulin_unit_shortname)).thenReturn("U") - whenever(rh.gs(app.aaps.core.ui.R.string.cancel_temp)).thenReturn("Cancel temp basal") - whenever(decimalFormatter.to2Decimal(any())).thenAnswer { - String.format("%.2f", it.arguments[0] as Double) - } - } - - @Test - fun `toHtml with queued result shows waiting message`() { - val result = TestPumpEnactResult().apply { - success = true - queued = true - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).isEqualTo("Waiting for pump result") - } - - @Test - fun `toHtml with simple success shows success status`() { - val result = TestPumpEnactResult().apply { - success = true - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Success: true") - } - - @Test - fun `toHtml with simple failure shows success status`() { - val result = TestPumpEnactResult().apply { - success = false - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Success: false") - } - - @Test - fun `toHtml with bolus delivered shows bolus information`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - bolusDelivered = 5.0 - comment = "Meal bolus" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Success: true") - assertThat(html).contains("Enacted: true") - assertThat(html).contains("Comment: Meal bolus") - assertThat(html).contains("SMB: 5.0 U") - } - - @Test - fun `toHtml with temp basal cancel shows cancel message`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - isTempCancel = true - comment = "Cancelled by user" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Enacted: true") - assertThat(html).contains("Comment: Cancelled by user") - assertThat(html).contains("Cancel temp basal") - } - - @Test - fun `toHtml with percent temp basal shows percent information`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - isPercent = true - percent = 120 - duration = 30 - comment = "High temp" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Enacted: true") - assertThat(html).contains("Comment: High temp") - assertThat(html).contains("Duration: 30 min") - assertThat(html).contains("Percent: 120%") - } - - @Test - fun `toHtml with absolute temp basal shows absolute information`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - absolute = 1.5 - duration = 30 - comment = "Moderate temp" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Enacted: true") - assertThat(html).contains("Comment: Moderate temp") - assertThat(html).contains("Duration: 30 min") - assertThat(html).contains("Absolute: 1.50 U/h") - } - - @Test - fun `toHtml with not enacted but with comment shows comment`() { - val result = TestPumpEnactResult().apply { - success = false - enacted = false - comment = "Pump not reachable" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Success: false") - assertThat(html).contains("Comment: Pump not reachable") - } - - @Test - fun `toHtml without comment does not include comment line`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - isPercent = true - percent = 100 - duration = 30 - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).doesNotContain("Comment:") - } - - @Test - fun `toHtml with bolus and empty comment does not include comment line`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - bolusDelivered = 3.0 - comment = "" - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("SMB: 3.0 U") - assertThat(html).doesNotContain("Comment:") - } - - @Test - fun `toHtml with absolute value of -1 is not shown`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - absolute = -1.0 - comment = "Some action" - } - - val html = result.toHtml(rh, decimalFormatter) - - // When enacted=true but no when case matches, comment is not shown - assertThat(html).isEqualTo("Success: true") - assertThat(html).doesNotContain("Absolute:") - assertThat(html).doesNotContain("Comment:") - } - - @Test - fun `toHtml with percent -1 is not shown`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - isPercent = true - percent = -1 - comment = "Some action" - } - - val html = result.toHtml(rh, decimalFormatter) - - // When enacted=true but no when case matches, comment is not shown - assertThat(html).isEqualTo("Success: true") - assertThat(html).doesNotContain("Percent:") - assertThat(html).doesNotContain("Comment:") - } - - @Test - fun `toHtml with zero bolus delivered shows bolus`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - bolusDelivered = 0.0 - } - - val html = result.toHtml(rh, decimalFormatter) - - // bolusDelivered > 0 check should exclude this - assertThat(html).doesNotContain("SMB:") - } - - @Test - fun `toHtml with very small bolus shows bolus`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - bolusDelivered = 0.1 - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("SMB: 0.1 U") - } - - @Test - fun `toHtml formats absolute value to 2 decimals`() { - val result = TestPumpEnactResult().apply { - success = true - enacted = true - absolute = 2.456 - duration = 30 - } - - val html = result.toHtml(rh, decimalFormatter) - - assertThat(html).contains("Absolute: 2.46 U/h") - } -} diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt index 622ccd9fc30c..8ca4cf23f6b6 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/AutomationRuntime.kt @@ -3,6 +3,11 @@ package app.aaps.plugins.automation import android.Manifest import android.content.Context import androidx.annotation.VisibleForTesting +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import app.aaps.core.data.format.NumberFormat import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.pump.defs.PumpType @@ -177,7 +182,9 @@ class AutomationRuntime @Inject constructor( @Volatile private var locationServiceRunning = false private val automationEvents = ArrayList() - var executionLog: MutableList = ArrayList() + // AnnotatedString, not HTML in a String: the only entry that carries formatting is built below, and + // the screen renders this list directly. Nothing here ever leaves the app. + var executionLog: MutableList = ArrayList() /** BT connect/disconnect events accumulated between processActions() runs (master only). The * single external reader is TriggerBTDevice, via [recentBtConnects]. */ @@ -494,7 +501,7 @@ class AutomationRuntime @Inject constructor( val runningMode = loop.runningMode() if (runningMode.pausesLoopExecution() || !runningMode.isLoopRunning()) { aapsLogger.debug(LTag.AUTOMATION, "Loop suspended") - executionLog.add(rh.gs(app.aaps.core.ui.R.string.loopsuspended)) + executionLog.add(AnnotatedString(rh.gs(app.aaps.core.ui.R.string.loopsuspended))) rxBus.send(EventAutomationUpdateGui()) commonEventsEnabled = false } @@ -503,7 +510,7 @@ class AutomationRuntime @Inject constructor( */ if (!(loop as PluginBase).isEnabled()) { aapsLogger.debug(LTag.AUTOMATION, "Loop not enabled") - executionLog.add(rh.gs(app.aaps.core.ui.R.string.disconnected)) + executionLog.add(AnnotatedString(rh.gs(app.aaps.core.ui.R.string.disconnected))) rxBus.send(EventAutomationUpdateGui()) commonEventsEnabled = false } @@ -513,7 +520,7 @@ class AutomationRuntime @Inject constructor( val enabled = constraintChecker.isAutomationEnabled() if (!enabled.value()) { val reason = enabled.getMostLimitedReasons() - if (executionLog.lastOrNull() != reason) executionLog.add(reason) + if (executionLog.lastOrNull()?.text != reason) executionLog.add(AnnotatedString(reason)) rxBus.send(EventAutomationUpdateGui()) commonEventsEnabled = false } @@ -549,21 +556,22 @@ class AutomationRuntime @Inject constructor( action.title = event.title if (action.isValid()) { val result = action.doAction() - val sb = StringBuilder() - .append(dateUtil.timeString(dateUtil.now())) - .append(" ") - .append(if (result.success) "☺" else "▼") - .append(" ") - .append(event.title) - .append(": ") - .append(action.shortDescription()) - .append(": ") - .append(result.comment) - executionLog.add(sb.toString()) - aapsLogger.debug(LTag.AUTOMATION, "Executed: $sb") + val entry = buildAnnotatedString { + append(dateUtil.timeString(dateUtil.now())) + append(" ") + append(if (result.success) "☺" else "▼") + append(" ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append("${event.title}:") } + append(" ") + append(action.shortDescription()) + append(": ") + append(result.comment) + } + executionLog.add(entry) + aapsLogger.debug(LTag.AUTOMATION, "Executed: ${entry.text}") rxBus.send(EventAutomationUpdateGui()) } else { - executionLog.add("Invalid action: ${action.shortDescription()}") + executionLog.add(AnnotatedString("Invalid action: ${action.shortDescription()}")) aapsLogger.debug(LTag.AUTOMATION, "Invalid action: ${action.shortDescription()}") rxBus.send(EventAutomationUpdateGui()) } diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationScreen.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationScreen.kt index e10a9d8626a6..cd7298758c71 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationScreen.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.core.text.HtmlCompat import app.aaps.core.ui.compose.AapsFab import app.aaps.core.ui.compose.AapsSpacing import app.aaps.core.ui.compose.MasterOfflineBanner @@ -90,7 +89,7 @@ fun AutomationScreen( ) } LogPanel( - logHtml = state.logHtml, + log = state.log, modifier = Modifier.fillMaxWidth() ) } @@ -294,7 +293,7 @@ private fun IconRow(event: AutomationEventUi) { } @Composable -private fun LogPanel(logHtml: String, modifier: Modifier = Modifier) { +private fun LogPanel(log: AnnotatedString, modifier: Modifier = Modifier) { Surface( color = MaterialTheme.colorScheme.surfaceContainerLow, modifier = modifier @@ -309,7 +308,7 @@ private fun LogPanel(logHtml: String, modifier: Modifier = Modifier) { .padding(horizontal = AapsSpacing.large, vertical = AapsSpacing.medium) ) { Text( - text = htmlToAnnotated(logHtml), + text = log, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -318,12 +317,6 @@ private fun LogPanel(logHtml: String, modifier: Modifier = Modifier) { } } -private fun htmlToAnnotated(html: String): AnnotatedString { - if (html.isEmpty()) return AnnotatedString("") - val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) - return AnnotatedString(spanned.toString()) -} - // ---------- Previews ---------- private fun sampleState() = @@ -366,7 +359,7 @@ private fun sampleState() = actionIcons = listOf(AutomationIcon(IcAutomation)) ) ), - logHtml = "12:00 Morning wakeup TT triggered
12:05 Snack reminder dismissed" + log = AnnotatedString("12:00 Morning wakeup TT triggered\n12:05 Snack reminder dismissed") ) @androidx.compose.ui.tooling.preview.Preview(showBackground = true, widthDp = 380, heightDp = 640) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationState.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationState.kt index 7652c4c614e7..3ee208ead103 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationState.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationState.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.automation.compose +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.graphics.vector.ImageVector import app.aaps.core.interfaces.navigation.ElementType @@ -56,5 +57,5 @@ data class AutomationEventUi( data class AutomationUiState( val events: List = emptyList(), - val logHtml: String = "" + val log: AnnotatedString = AnnotatedString("") ) diff --git a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt index ce938bc823a7..77d3949f5906 100644 --- a/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt +++ b/plugins/automation/src/main/kotlin/app/aaps/plugins/automation/compose/AutomationStateHolder.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.automation.compose +import androidx.compose.ui.text.AnnotatedString import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.rx.bus.RxBus @@ -287,11 +288,14 @@ class AutomationStateHolder( actionIcons = actionIcons.distinct() ) } - val sb = StringBuilder() - for (l in plugin.executionLog.reversed()) sb.append(l).append("
") + val sb = AnnotatedString.Builder() + for (l in plugin.executionLog.reversed()) { + sb.append(l) + sb.append('\n') + } _state.value = _state.value.copy( events = events, - logHtml = sb.toString() + log = sb.toAnnotatedString() ) } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/compose/NSClientComposeContent.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/compose/NSClientComposeContent.kt index 5b206ba6e169..f59a57748ff9 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/compose/NSClientComposeContent.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/compose/NSClientComposeContent.kt @@ -7,6 +7,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import app.aaps.core.data.ue.Action @@ -52,7 +57,7 @@ class NSClientComposeContent( var showFullSyncDialog by remember { mutableStateOf(false) } var showCleanupDialog by remember { mutableStateOf(false) } var showResultDialog by remember { mutableStateOf(false) } - var resultMessage by remember { mutableStateOf("") } + var resultMessage by remember { mutableStateOf(AnnotatedString("")) } // Load initial data LaunchedEffect(Unit) { viewModel.loadInitialData() } @@ -86,7 +91,11 @@ class NSClientComposeContent( persistenceLayer.cleanupDatabase(CLEANUP_RETENTION_DAYS, deleteTrackedChanges = true) } if (result.isNotEmpty()) { - resultMessage = "$clearedEntriesText
$result" + resultMessage = buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(clearedEntriesText) } + appendLine() + append(result) + } showResultDialog = true } aapsLogger.info(LTag.CORE, "Cleaned up databases with result: $result") diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSDeviceStatusHandler.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSDeviceStatusHandler.kt index d27aeff80737..8b492acea4df 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSDeviceStatusHandler.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/data/NSDeviceStatusHandler.kt @@ -160,7 +160,13 @@ class NSDeviceStatusHandler @Inject constructor( } pump.extended?.let { val extended = StringBuilder() - it.keys.forEach { key -> extended.append("").append(key).append(": ").append(it[key]).append("
") } + // Plain text, not HTML. This used to build "key: value
" and the overview + // stripped the markup straight back out before showing it. + // + // The values are whatever uploaded this device status - another AAPS, an older one, + // or a different app entirely - so anything they marked up is flattened here, at the + // point the foreign data arrives, rather than at every place that displays it. + it.keys.forEach { key -> extended.appendLine("$key: ${it[key].toPlainText()}") } deviceStatusPumpData.extended = extended.toString() deviceStatusPumpData.activeProfileName = it.safeGetStringAllowNull("ActiveProfile", null) } @@ -217,4 +223,15 @@ class NSDeviceStatusHandler @Inject constructor( processedDeviceStatusData.uploaderMap[device] = uploader } } -} \ No newline at end of file +} +/** + * Flattens any markup a foreign uploader put in a device-status value. + * + * Kept identical to what the overview used to do before showing the text, so nothing that renders + * correctly today starts showing raw tags. + */ +private fun Any?.toPlainText(): String = + toString() + .replace("
", "\n") + .replace(Regex("<[^>]*>"), "") + .replace(" ", " ") diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt index 5e12cdae33af..f2860186691d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tidepool/events/EventTidepoolStatus.kt @@ -1,22 +1,8 @@ package app.aaps.plugins.sync.tidepool.events import app.aaps.core.interfaces.rx.events.Event -import java.text.SimpleDateFormat -import java.util.Locale data class EventTidepoolStatus(val status: String) : Event() { var date: Long = System.currentTimeMillis() - - private var timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - - fun toPreparedHtml(): StringBuilder { - val stringBuilder = StringBuilder() - stringBuilder.append(timeFormat.format(date)) - stringBuilder.append(" ") - stringBuilder.append(status) - stringBuilder.append(" ") - stringBuilder.append("
") - return stringBuilder - } } \ No newline at end of file diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightAlertService.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightAlertService.kt index e7a9cae15533..66ef0b952c7a 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightAlertService.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/InsightAlertService.kt @@ -17,7 +17,6 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus -import app.aaps.core.utils.HtmlHelper import app.aaps.pump.insight.app_layer.activities.InsightAlertActivity import app.aaps.pump.insight.app_layer.remote_control.ConfirmAlertMessage import app.aaps.pump.insight.app_layer.remote_control.SnoozeAlertMessage @@ -251,7 +250,7 @@ class InsightAlertService : DaggerService(), InsightConnectionService.StateCallb notificationBuilder.setSmallIcon(app.aaps.core.ui.R.drawable.notif_icon) alert.alertType?.let { notificationBuilder.setContentTitle(alertUtils.getAlertCode(it) + " – " + alertUtils.getAlertTitle(it)) } val description = alertUtils.getAlertDescription(alert) - if (description != null) notificationBuilder.setContentText(HtmlHelper.fromHtml(description).toString()) + if (description != null) notificationBuilder.setContentText(description) val fullScreenIntent = Intent(this, InsightAlertActivity::class.java) val fullScreenPendingIntent = PendingIntent.getActivity(this, 0, fullScreenIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) notificationBuilder.setFullScreenIntent(fullScreenPendingIntent, true) diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/app_layer/activities/InsightAlertActivity.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/app_layer/activities/InsightAlertActivity.kt index 583775897c60..ebf126ee9435 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/app_layer/activities/InsightAlertActivity.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/app_layer/activities/InsightAlertActivity.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.Modifier import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.ui.compose.LocalSnackbarHostState import app.aaps.core.ui.compose.dialogs.GlobalSnackbarHost -import app.aaps.core.utils.HtmlHelper import app.aaps.pump.insight.InsightAlertService import app.aaps.pump.insight.compose.InsightAlertScreen import app.aaps.pump.insight.compose.InsightAlertUiState @@ -105,7 +104,7 @@ class InsightAlertActivity : DaggerAppCompatActivity() { val icon = alert.alertCategory?.let { alertUtils.getAlertIcon(it) } ?: Icons.Default.Error val errorCode = alert.alertType?.let { alertUtils.getAlertCode(it) } ?: "" val title = alert.alertType?.let { alertUtils.getAlertTitle(it) } ?: "" - val description = alertUtils.getAlertDescription(alert)?.let { HtmlHelper.fromHtml(it) } + val description = alertUtils.getAlertDescription(alert) state = InsightAlertUiState( icon = icon, errorCode = errorCode, diff --git a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightAlertScreen.kt b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightAlertScreen.kt index 3f1e6f5be2ea..32b46bb357e0 100644 --- a/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightAlertScreen.kt +++ b/pump/insight/src/main/kotlin/app/aaps/pump/insight/compose/InsightAlertScreen.kt @@ -32,7 +32,7 @@ data class InsightAlertUiState( val icon: ImageVector, val errorCode: String, val title: String, - val description: CharSequence?, + val description: String?, val alertStatus: AlertStatus?, val muteEnabled: Boolean, val confirmEnabled: Boolean @@ -70,7 +70,7 @@ fun InsightAlertScreen( ) state.description?.let { desc -> Text( - text = desc.toString(), + text = desc, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center ) diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 2f8fc4cfdd47..c9b16da896f6 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -699,7 +699,7 @@ class OmnipodDashPumpPlugin @Inject constructor( notifyOnUnconfirmed( NotificationId.OMNIPOD_UNCERTAIN_SMB, "Unable to verify whether SMB bolus ($requestedBolusAmount U) succeeded. " + - "Refresh pod status to confirm or deny this command.", + "Refresh pod status to confirm or deny this command.", AlarmSound.BOLUS_ERROR ) } else { diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt index 2a6607df4393..d1a9af0bbf24 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/insulinManagement/InsulinManagementScreen.kt @@ -49,6 +49,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import app.aaps.core.ui.compose.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -525,8 +529,12 @@ private fun PeakPresetChips( val parts = presets.map { preset -> Triple(stringResource(preset.label), stringResource(preset.comment), stringResource(CoreUiR.string.format_mins, preset.iCfg.peak)) } - val message = parts.joinToString("\n\n") { (label, comment, peak) -> - "$label\n$comment — $peak" + val message = buildAnnotatedString { + parts.forEachIndexed { index, (label, comment, peak) -> + if (index > 0) append("\n\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(label) } + append("\n$comment — $peak") + } } OkDialog( title = stringResource(R.string.load_peak_from), diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt index beb0bee68875..0d860f4cecbd 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/maintenance/MaintenanceDialogs.kt @@ -7,6 +7,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import app.aaps.core.ui.compose.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.aaps.core.keys.StringKey @@ -226,7 +230,11 @@ fun MaintenanceDialogs( cleanupResultText?.let { result -> OkDialog( title = stringResource(CoreUiR.string.result), - message = "" + stringResource(CoreUiR.string.cleared_entries) + "
" + result, + message = buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(stringResource(CoreUiR.string.cleared_entries)) } + appendLine() + append(result) + }, onDismiss = { cleanupResultText = null } ) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt index 6b73cc3f34bb..dea4c859695c 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/overview/OverviewDataCacheImpl.kt @@ -977,7 +977,7 @@ class OverviewDataCacheImpl @AssistedInject constructor( }.trim() val dialogText = buildString { pumpData.extended?.let { - append(it.replace("
", "\n").replace(Regex("<[^>]*>"), "").replace(" ", " ").trim()) + append(it.trim()) } } AapsClientStatusItem( From e63983a3ffa8aa4408e799c765992b0c378c107a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 16:14:04 +0200 Subject: [PATCH 118/146] :core:ui reaches 96% commonMain 426 of 444 files, up from 348. Three of the 18 left are .android.kt actuals, so 15 are real Android code. ## What moved, and why it was stuck ProfileUtil was the cheapest win in the whole exercise: the interface imports nothing but :core:data, so it was simply in the wrong source set. Moving it also cleared the ProfileUtil error out of AapsTheme, PreferenceState and TemporaryTargetDisplay. AapsTheme looked like the hard one and was not. Three lines of 317 were Android: the system-bar icon SideEffect, LocalConfiguration for tablet detection, and one includeFontPadding. Two small expects - SystemBarAppearance and smallestScreenWidthDp - and the theme itself moved, pulling 26 files with it because most of the UI sits under it. BackHandler needed no BOM bump. androidx compose-ui has no backhandler package at any version we resolve (checked 1.10.0, 1.11.2, 1.11.4). It is a separate CMP artifact, org.jetbrains.compose.ui:ui-backhandler, which ships its own Android variant. It is still experimental, hence the OptIn at the two call sites. Six files narrowed from ResourceHelper to TextResolver, four more carried dead Android imports - @StringRes on files already returning TextRef, and androidx.compose.ui.res on files already passing one. ## Rounding AdaptiveUnitDoublePreference and PreferenceState used BigDecimal(v).setScale(n, HALF_UP). The multiplatform NumberFormat is half-even, so switching to it would have quietly changed a displayed glucose value. The two modes can only disagree on an exact tie. At one decimal - mmol/L - a tie would have to be (2k+1)/20, and the factor of 5 means no Double ever lands there, so they cannot disagree at all. At zero decimals - mg/dL - a tie is k + 0.5, which IS exactly representable, and there half-even gives 4.5 -> 4. Half-even is banker's rounding: it cancels bias when many values are summed, which is not what a single number on a preference screen does. So half-up stays, and NumberFormat learned the mode instead: a NumberRounding enum, a rounding field defaulting to HALF_EVEN so nothing else changes, wired through all three actuals (DecimalFormat.roundingMode, NSNumberFormatterRoundHalfUp, and the mingw tie branch). SEPARATOR_DOT at the call sites, because toPlainString was always dot-separated and the text is parsed back when edited. ## The regression the emulator caught Naming the shared builders' strings with UiStrings broke them. ResourceHelper lives in :core:interfaces and cannot see UiStringIds - :core:ui depends on it, not the reverse - so a `ui` name read outside a Composable fell back to printing its own name, and the overview showed "format_carbs" instead of "58 g". The KDoc on keysIdOf had called this exactly: "If a non-Compose caller ever needs a `ui` name, this is the place that has to learn about it - at that point a registry is probably better than a third branch." So: TextRefIdRegistry, consulted for any owner keysIdOf does not know directly, with ResourceHelperImpl registering "ui" in its init - that class is downstream of every string-owning module and is built before anything can ask it for text. Verified on emulator: the row reads "58 g", and Treatments and Manage carry no stray names either. Four pump/source tests needed both gs overloads stubbed, not one swapped for the other: the shared builders call gs(TextRef) while the plugins' own code still calls gs(Int). Not verified by hand: the rounding change. It only differs on exact .5 ties, which is the behaviour deliberately preserved, but it is a glucose display. --- .../app/aaps/core/data/format/NumberFormat.kt | 19 +++++++++--- .../aaps/core/data/format/NumberRounding.kt | 28 +++++++++++++++++ .../data/format/NumberFormatPlatform.ios.kt | 8 ++++- .../data/format/NumberFormatPlatform.jvm.kt | 7 +++-- .../data/format/NumberFormatPlatform.mingw.kt | 11 +++---- .../interfaces/resources/ResourceHelper.kt | 14 ++++----- .../interfaces/resources/TextRefIdRegistry.kt | 30 +++++++++++++++++++ .../core/interfaces/profile/ProfileUtil.kt | 0 core/ui/build.gradle.kts | 3 ++ .../core/ui/compose/PlatformTheme.android.kt | 24 +++++++++++++++ .../preference/AdaptivePreferenceItem.kt | 4 +-- .../AdaptiveUnitDoublePreference.kt | 5 ++-- .../preference/PluginPreferencesScreen.kt | 6 +++- .../ui/clientcontrol/FailureReasonText.kt | 1 - .../app/aaps/core/ui/compose/AapsTheme.kt | 20 ++----------- .../app/aaps/core/ui/compose/CarbTimeRow.kt | 0 .../app/aaps/core/ui/compose/FormatUtils.kt | 13 ++++---- .../core/ui/compose/MasterOfflineBanner.kt | 0 .../aaps/core/ui/compose/NumberInputRow.kt | 0 .../core/ui/compose/NumberInputRowPreviews.kt | 0 .../app/aaps/core/ui/compose/PlatformTheme.kt | 23 ++++++++++++++ .../core/ui/compose/PluginCategoryTitle.kt | 1 - .../app/aaps/core/ui/compose/PlusMinusEdit.kt | 0 .../core/ui/compose/SelectableListToolbar.kt | 25 ++++++++-------- .../aaps/core/ui/compose/SliderWithButtons.kt | 0 .../ui/compose/SliderWithButtonsPreviews.kt | 0 .../app/aaps/core/ui/compose/StatusLevel.kt | 0 .../aaps/core/ui/compose/TimeRangePicker.kt | 0 .../ui/compose/dialogs/ConfirmationMessage.kt | 0 .../dialogs/ElementConfirmationDialog.kt | 0 .../ui/compose/dialogs/GlobalDialogHost.kt | 0 .../ui/compose/dialogs/GlobalSnackbarHost.kt | 0 .../ui/compose/dialogs/ValueInputDialog.kt | 0 .../dialogs/ValueInputDialogPreviews.kt | 0 .../compose/insulin/ConcentrationDropDown.kt | 1 - .../core/ui/compose/insulin/SelectInsulin.kt | 0 .../compose/insulin/SelectInsulinPreviews.kt | 0 .../ui/compose/navigation/ElementTypeStyle.kt | 0 .../ui/compose/pickers/HourWheelPicker.kt | 0 .../preference/AdaptiveDoublePreference.kt | 0 .../AdaptiveDoublePreferencePreviews.kt | 0 .../preference/AdaptiveIntPreference.kt | 0 .../AdaptiveIntPreferencePreviews.kt | 0 .../preference/AdaptiveIntentPreference.kt | 0 .../preference/AdaptiveListPreference.kt | 0 .../AdaptiveListPreferencePreviews.kt | 0 .../AdaptiveMasterPasswordPreference.kt | 0 ...daptiveMasterPasswordPreferencePreviews.kt | 0 .../preference/AdaptivePasswordPreference.kt | 0 .../AdaptivePasswordPreferencePreviews.kt | 0 .../preference/AdaptiveStringPreference.kt | 0 .../AdaptiveStringPreferencePreviews.kt | 0 .../preference/AdaptiveSwitchPreference.kt | 0 .../AdaptiveSwitchPreferencePreviews.kt | 0 .../ClickablePreferenceCategoryHeader.kt | 0 .../CollapsibleCardSectionContent.kt | 0 .../CollapsibleCardSectionContentPreviews.kt | 0 .../preference/InlinePreferenceItems.kt | 0 .../ui/compose/preference/ListPreference.kt | 0 .../preference/ListPreferencePreviews.kt | 0 .../core/ui/compose/preference/Preference.kt | 0 .../preference/PreferenceAlertDialog.kt | 0 .../compose/preference/PreferenceCategory.kt | 0 .../preference/PreferenceCategoryPreviews.kt | 0 .../compose/preference/PreferencePreviews.kt | 0 .../preference/PreferenceSliderWithButtons.kt | 0 .../ui/compose/preference/PreferenceState.kt | 7 +++-- .../ui/compose/preference/PreferenceTheme.kt | 0 .../ui/compose/preference/PreviewUtils.kt | 0 .../ui/compose/preference/SwitchPreference.kt | 0 .../preference/SwitchPreferencePreviews.kt | 0 .../core/ui/compose/preference/SyncBadge.kt | 0 .../compose/preference/TextFieldPreference.kt | 0 .../preference/TextFieldPreferencePreviews.kt | 0 .../compose/pump/PumpCommunicationStatus.kt | 9 +++--- .../ui/compose/pump/PumpOverviewModels.kt | 0 .../ui/compose/pump/PumpOverviewScreen.kt | 0 .../compose/pump/PumpOverviewStateBuilder.kt | 21 +++++++------ .../aaps/core/ui/compose/pump/WizardScreen.kt | 4 ++- .../ui/compose/siteRotation/SiteEntryList.kt | 0 .../siteRotation/SiteEntryListPreviews.kt | 0 .../siteRotation/SiteLocationPicker.kt | 0 .../SiteLocationPickerPreviews.kt | 0 .../siteRotation/SiteLocationPickerScreen.kt | 0 .../SiteLocationPickerScreenPreviews.kt | 0 .../siteRotation/SiteLocationWizardStep.kt | 0 .../SiteLocationWizardStepPreviews.kt | 0 .../aaps/core/ui/extensions/CobInfoDisplay.kt | 8 ++--- .../ui/extensions/TemporaryTargetDisplay.kt | 8 ++--- .../app/aaps/core/ui/search/SearchableItem.kt | 0 .../aaps/core/ui/search/SearchableProvider.kt | 0 .../aaps/core/ui/compose/PlatformTheme.ios.kt | 21 +++++++++++++ gradle/libs.versions.toml | 1 + .../resources/ResourceHelperImpl.kt | 9 ++++++ .../source/compose/BgSourceScreenTest.kt | 8 +++-- .../compose/EopatchOverviewViewModelTest.kt | 7 +++++ .../compose/MedtrumOverviewViewModelTest.kt | 7 +++++ 97 files changed, 257 insertions(+), 96 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberRounding.kt create mode 100644 core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/TextRefIdRegistry.kt rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt (100%) create mode 100644 core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.android.kt rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/AapsTheme.kt (93%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/FormatUtils.kt (87%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt (100%) create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.kt rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt (96%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt (88%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/StatusLevel.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/Preference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt (98%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt (89%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt (82%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt (97%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt (78%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt (79%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/search/SearchableItem.kt (100%) rename core/ui/src/{androidMain => commonMain}/kotlin/app/aaps/core/ui/search/SearchableProvider.kt (100%) create mode 100644 core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.ios.kt diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt index 0734a7cdc1d5..4edca1bdd939 100644 --- a/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberFormat.kt @@ -27,7 +27,8 @@ package app.aaps.core.data.format class NumberFormat( val minIntegerDigits: Int = 1, val minFractionDigits: Int = 0, - val maxFractionDigits: Int = minFractionDigits + val maxFractionDigits: Int = minFractionDigits, + val rounding: NumberRounding = NumberRounding.HALF_EVEN ) { init { @@ -55,11 +56,12 @@ class NumberFormat( other is NumberFormat && minIntegerDigits == other.minIntegerDigits && minFractionDigits == other.minFractionDigits && - maxFractionDigits == other.maxFractionDigits + maxFractionDigits == other.maxFractionDigits && + rounding == other.rounding - override fun hashCode(): Int = (minIntegerDigits * 31 + minFractionDigits) * 31 + maxFractionDigits + override fun hashCode(): Int = ((minIntegerDigits * 31 + minFractionDigits) * 31 + maxFractionDigits) * 31 + rounding.ordinal - override fun toString(): String = "NumberFormat($minIntegerDigits, $minFractionDigits, $maxFractionDigits)" + override fun toString(): String = "NumberFormat($minIntegerDigits, $minFractionDigits, $maxFractionDigits, $rounding)" companion object { @@ -96,6 +98,15 @@ class NumberFormat( /** Three decimals, plus a fourth one when it is not zero. Old pattern `"0.000#"`. */ val DECIMAL_3_UP_TO_4 = NumberFormat(minFractionDigits = 3, maxFractionDigits = 4) + /** + * Fixed decimals, ties away from zero. + * + * For a single value on screen, where the reader expects .5 to go up rather than to the + * even neighbour. See [NumberRounding]. + */ + fun withDecimalsHalfUp(decimals: Int): NumberFormat = + NumberFormat(minFractionDigits = decimals, rounding = NumberRounding.HALF_UP) + /** Format with the given number of fixed decimals. */ fun withDecimals(decimals: Int): NumberFormat = when (decimals) { 0 -> INTEGER diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberRounding.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberRounding.kt new file mode 100644 index 000000000000..ed7cd4e45067 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/format/NumberRounding.kt @@ -0,0 +1,28 @@ +package app.aaps.core.data.format + +/** + * What to do with a value that sits exactly halfway between two renderable ones. + * + * Only reachable when the halfway point is exactly representable as a `Double`, which is why the + * choice matters far less often than it looks. Rounding to whole numbers has reachable ties, because + * `x.5` is a dyadic rational. Rounding to one decimal does not: a tie there would have to be + * `(2k+1)/20`, and the factor of 5 in the denominator means no `Double` ever lands on it. + */ +enum class NumberRounding { + + /** + * Ties go to the even neighbour: `0.5` renders as `0`, `1.5` as `2`. + * + * Banker's rounding. It exists to stop a bias from building up when many rounded values are + * summed, and it is the default because it is what `DecimalFormat` has always done here. + */ + HALF_EVEN, + + /** + * Ties go away from zero: `0.5` renders as `1`. + * + * What a reader expects from a single number on screen, so it is the right choice for a value + * shown on its own rather than one that will be added up. + */ + HALF_UP +} diff --git a/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt b/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt index 8d354ce6696b..f578a7940041 100644 --- a/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt +++ b/core/data/src/iosMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.ios.kt @@ -5,6 +5,7 @@ import platform.Foundation.NSNumber import platform.Foundation.NSNumberFormatter import platform.Foundation.NSNumberFormatterDecimalStyle import platform.Foundation.NSNumberFormatterRoundHalfEven +import platform.Foundation.NSNumberFormatterRoundHalfUp import platform.Foundation.currentLocale /** @@ -36,7 +37,12 @@ actual object NumberFormatPlatform { setMaximumFractionDigits(format.maxFractionDigits.toULong()) // Old patterns like "0.00" never grouped, and NSNumberFormatter groups by default. setUsesGroupingSeparator(false) - setRoundingMode(NSNumberFormatterRoundHalfEven) + setRoundingMode( + when (format.rounding) { + NumberRounding.HALF_EVEN -> NSNumberFormatterRoundHalfEven + NumberRounding.HALF_UP -> NSNumberFormatterRoundHalfUp + } + ) setDecimalSeparator(separator.toString()) }.stringFromNumber(NSNumber(double = value)) ?: "" diff --git a/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt b/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt index eec8d27f15c1..2d1ce5195f1c 100644 --- a/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt +++ b/core/data/src/jvmMain/kotlin/app/aaps/core/data/format/NumberFormatPlatform.jvm.kt @@ -38,8 +38,11 @@ actual object NumberFormatPlatform { maximumFractionDigits = key.format.maxFractionDigits // Old patterns like "0.00" never grouped. DecimalFormat() does by default. isGroupingUsed = false - // Same as the default of DecimalFormat, set here so it cannot drift. - roundingMode = RoundingMode.HALF_EVEN + // HALF_EVEN is the DecimalFormat default, set here so it cannot drift. + roundingMode = when (key.format.rounding) { + NumberRounding.HALF_EVEN -> RoundingMode.HALF_EVEN + NumberRounding.HALF_UP -> RoundingMode.HALF_UP + } decimalFormatSymbols = DecimalFormatSymbols.getInstance(key.locale).apply { decimalSeparator = key.separator } } } diff --git a/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt index cd8d0d02be29..039b7f93ef1f 100644 --- a/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt +++ b/core/data/src/mingwX64Main/kotlin/app/aaps/core/data/format/NumberFormatPlatform.mingw.kt @@ -28,14 +28,15 @@ actual object NumberFormatPlatform { val negative = value < 0 || (value == 0.0 && 1.0 / value < 0) val scale = 10.0.pow(format.maxFractionDigits) val scaled = abs(value) * scale - // half-even, same as DecimalFormat val floor = truncate(scaled) val rest = scaled - floor val rounded = when { - rest > 0.5 -> floor + 1 - rest < 0.5 -> floor - floor.toLong() % 2L == 0L -> floor - else -> floor + 1 + rest > 0.5 -> floor + 1 + rest < 0.5 -> floor + // Exactly halfway. Where the two modes differ - see NumberRounding. + format.rounding == NumberRounding.HALF_UP -> floor + 1 + floor.toLong() % 2L == 0L -> floor + else -> floor + 1 } var whole = truncate(rounded / scale).toLong().toString() diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt index f62c14b7a282..246a864408e1 100644 --- a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/ResourceHelper.kt @@ -59,17 +59,15 @@ interface ResourceHelper : TextResolver { /** * Resolves a [TextRef.Named] that this module can see. * - * Two owners are resolvable here: `keys`, via the `:core:keys` dependency, and `interfaces`, whose - * map is generated into this module. A name owned by another module falls back to showing the raw - * name - visibly wrong rather than silently blank. + * Two owners are resolvable directly: `keys`, via the `:core:keys` dependency, and `interfaces`, + * whose map is generated into this module. * - * That is not a gap in practice today: the `ui`-owned names are used from Composables, and - * `app.aaps.core.ui.compose.stringResource` sits in `:core:ui`, which can see all three maps. If a - * non-Compose caller ever needs a `ui` name, this is the place that has to learn about it - at that - * point a registry is probably better than a third branch. + * Anything else is asked of [TextRefIdRegistry], which is how a module further up - `:core:ui` and + * its `ui` names - makes itself resolvable here. Only when nobody has claimed the owner does this + * fall back to showing the raw name, which is visibly wrong rather than silently blank. */ private fun keysIdOf(ref: TextRef.Named): Int? = when (ref.owner) { "keys" -> KeysStringIds.idOf(ref.name) "interfaces" -> InterfacesStringIds.idOf(ref.name) - else -> null + else -> TextRefIdRegistry.idOf(ref) } diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/TextRefIdRegistry.kt b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/TextRefIdRegistry.kt new file mode 100644 index 000000000000..9bda921ee230 --- /dev/null +++ b/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/resources/TextRefIdRegistry.kt @@ -0,0 +1,30 @@ +package app.aaps.core.interfaces.resources + +import app.aaps.core.keys.interfaces.TextRef + +/** + * Where a module tells the rest of the app how to resolve the names it owns. + * + * `ResourceHelper` can only see the id maps of the modules it depends on - `keys` and `interfaces`. + * A name owned by a module further up, such as `ui`, is invisible to it, and the fallback is to show + * the raw name. That is fine while such names are only read from Composables, because + * `app.aaps.core.ui.compose.stringResource` lives in `:core:ui` and can see all of them. It stops + * being fine the moment a non-Compose caller wants one - which is what happened when the shared + * builders started naming their strings instead of numbering them, and `format_carbs` appeared on + * the overview instead of "12 g". + * + * Registration happens once, from `ResourceHelperImpl`, which is downstream of every module that + * owns strings and is constructed before anything can ask it for text. + */ +object TextRefIdRegistry { + + private val lookups = mutableMapOf Int?>() + + /** Teaches the resolver how to turn a name owned by [owner] into a resource id. */ + fun register(owner: String, idOf: (String) -> Int?) { + lookups[owner] = idOf + } + + /** The id for [ref], or null when no module has claimed that owner. */ + fun idOf(ref: TextRef.Named): Int? = lookups[ref.owner]?.invoke(ref.name) +} diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileUtil.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 081baddeb381..45a261630cb2 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -83,6 +83,9 @@ kotlin { api(libs.cmp.runtime) api(libs.cmp.foundation) api(libs.cmp.ui) + // Separate CMP artifact - androidx compose-ui has no backhandler package, so a BOM bump would + // not have helped. This one ships an Android variant of its own. + api(libs.cmp.ui.backhandler) api(libs.cmp.material3) api(libs.cmp.material.icons.extended) implementation(compose.components.uiToolingPreview) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.android.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.android.kt new file mode 100644 index 000000000000..1eb3402e3145 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.android.kt @@ -0,0 +1,24 @@ +package app.aaps.core.ui.compose + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowInsetsControllerCompat + +@Composable +actual fun SystemBarAppearance(isDark: Boolean) { + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + val controller = WindowInsetsControllerCompat(window, view) + controller.isAppearanceLightStatusBars = !isDark + controller.isAppearanceLightNavigationBars = !isDark + } + } +} + +@Composable +actual fun smallestScreenWidthDp(): Int = LocalConfiguration.current.smallestScreenWidthDp diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index bf2ddabbe456..350a0d2276c2 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -5,12 +5,12 @@ package app.aaps.core.ui.compose.preference -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import app.aaps.core.keys.PreferenceType +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.StringKey import app.aaps.core.keys.interfaces.BooleanPreferenceKey import app.aaps.core.keys.interfaces.DoublePreferenceKey @@ -179,7 +179,7 @@ fun AdaptivePreferenceItem( val resolvedClick = key.onClick ?: onIntentClick val resolvedCompose = key.composeScreen as? ComposeScreenContent val onNavigateToCompose = LocalNavigateToCompose.current - val resolvedUrl = key.runtimeUrl ?: intentUrl ?: key.urlResId?.let { stringResource(it) } + val resolvedUrl = key.runtimeUrl ?: intentUrl ?: key.urlResId?.let { stringResource(TextRef.AndroidRes(it)) } when { resolvedClick != null -> { diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt index 3b93b2e38b5c..b0ef5cde09bd 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveUnitDoublePreference.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import app.aaps.core.data.format.NumberFormat +import app.aaps.core.data.format.NumberFormatPlatform import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.keys.interfaces.UnitDoublePreferenceKey import app.aaps.core.keys.interfaces.VisibilityContext @@ -18,8 +19,6 @@ import app.aaps.core.ui.compose.LocalPreferences import app.aaps.core.ui.compose.LocalProfileUtil import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.compose.stringResourceOrNull -import java.math.BigDecimal -import java.math.RoundingMode import kotlin.math.abs import app.aaps.core.ui.R as UiR @@ -96,7 +95,7 @@ fun AdaptiveUnitDoublePreferenceItem( onValueChange = { newValue -> if (visibility.enabled) { // Format with appropriate precision and update state - val formatted = BigDecimal(newValue).setScale(decimalPlaces, RoundingMode.HALF_UP).toPlainString() + val formatted = NumberFormat.withDecimalsHalfUp(decimalPlaces).format(newValue, NumberFormatPlatform.SEPARATOR_DOT) state.updateDisplayValue(formatted) } }, diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt index 1f8b2d062726..a0a2fb53aa3e 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PluginPreferencesScreen.kt @@ -2,7 +2,8 @@ package app.aaps.core.ui.compose.preference import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings -import androidx.activity.compose.BackHandler +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -42,6 +43,9 @@ import kotlinx.coroutines.launch * @param visibilityContext Context for evaluating visibility conditions * @param onBackClick Callback when back button is clicked */ +// BackHandler is still marked experimental in Compose Multiplatform. The Android behaviour is +// unchanged - it is the same predictive-back plumbing androidx.activity.compose.BackHandler used. +@OptIn(ExperimentalComposeUiApi::class) @Composable fun PluginPreferencesScreen( plugin: PluginBase, diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt index b5ceb1554102..b961e326abb3 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/clientcontrol/FailureReasonText.kt @@ -2,7 +2,6 @@ package app.aaps.core.ui.clientcontrol import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings -import androidx.annotation.StringRes import app.aaps.core.interfaces.clientcontrol.FailureReason /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt similarity index 93% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt index f9b5a98209d5..a19005d845e0 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/AapsTheme.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose -import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -12,17 +11,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.style.LineHeightStyle -import androidx.core.view.WindowInsetsControllerCompat import app.aaps.core.interfaces.configuration.Config import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.utils.DateUtil @@ -267,15 +261,7 @@ fun AapsTheme( // Keep system bar icon color in sync with the AAPS-effective theme so // status/nav bar icons stay legible against the bar scrims (which use // colorScheme.surface). Reactive — no activity recreate needed. - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - val controller = WindowInsetsControllerCompat(window, view) - controller.isAppearanceLightStatusBars = !isDark - controller.isAppearanceLightNavigationBars = !isDark - } - } + SystemBarAppearance(isDark) val scheme = if (isDark) darkColors else lightColors val profileViewerColors = if (isDark) DarkProfileHelperColors else LightProfileHelperColors @@ -284,7 +270,7 @@ fun AapsTheme( val snackbarColors = if (isDark) DarkSnackbarColors else LightSnackbarColors // Scale typography up on tablets. Orientation-independent (smallest-width signal). - val isTablet = LocalConfiguration.current.smallestScreenWidthDp >= TABLET_MIN_SW_DP + val isTablet = smallestScreenWidthDp() >= TABLET_MIN_SW_DP val typographyScale = if (isTablet) 1.5f else 1f val scaledMaterialTypography = remember(typographyScale) { Typography().withoutFontPadding().scaled(typographyScale) } @@ -304,7 +290,7 @@ fun AapsTheme( // Bare `Text()` (e.g. the overview chips) reads LocalTextStyle, not the typography, // so disable font padding here too — keeps the default size, just metric-centers glyphs. LocalTextStyle provides LocalTextStyle.current.copy( - platformStyle = PlatformTextStyle(includeFontPadding = false), + platformStyle = noFontPaddingPlatformStyle(), lineHeightStyle = LineHeightStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/CarbTimeRow.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt similarity index 87% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt index e34006b0d2a3..f2166581b6fc 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/FormatUtils.kt @@ -3,9 +3,8 @@ package app.aaps.core.ui.compose import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import app.aaps.core.data.format.NumberFormat -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.keys.interfaces.TextRef -import app.aaps.core.ui.R import kotlin.math.abs import kotlin.math.roundToInt @@ -29,18 +28,18 @@ fun formatMinutesAsDuration(minutes: Int): String { /** * Formats minutes as duration string: "X h Y min" when >= 60 (omits minutes if zero), "X min" otherwise. - * Non-composable version using ResourceHelper. + * Non-composable version using TextResolver. */ -fun formatMinutesAsDuration(minutes: Int, rh: ResourceHelper): String { +fun formatMinutesAsDuration(minutes: Int, rh: TextResolver): String { val abs = abs(minutes) val sign = if (minutes < 0) "-" else "" return if (abs >= 60) { val hours = abs / 60 val mins = abs % 60 - sign + if (mins == 0) rh.gs(R.string.format_hours_only, hours) - else rh.gs(R.string.format_hour_minute, hours, mins) + sign + if (mins == 0) rh.gs(UiStrings.format_hours_only, hours) + else rh.gs(UiStrings.format_hour_minute, hours, mins) } else { - rh.gs(R.string.format_mins, minutes) + rh.gs(UiStrings.format_mins, minutes) } } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/MasterOfflineBanner.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRow.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.kt new file mode 100644 index 000000000000..5e336a974503 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.kt @@ -0,0 +1,23 @@ +package app.aaps.core.ui.compose + +import androidx.compose.runtime.Composable + +/** + * Keeps the system bar icons legible against the bar scrims, which use `colorScheme.surface`. + * + * Platform specific because the two platforms do not even agree on what a system bar is: Android + * flips light/dark icon appearance on the window, and this must react without recreating the + * activity. + */ +@Composable +expect fun SystemBarAppearance(isDark: Boolean) + +/** + * Smallest screen width in dp, used to decide whether this is a tablet and scale typography. + * + * Smallest width rather than current width, so the answer does not change when the device is + * rotated. Platform specific because Android answers from the device configuration rather than from + * the size of the window the app happens to occupy. + */ +@Composable +expect fun smallestScreenWidthDp(): Int diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt similarity index 96% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt index 4b31af03c192..08ac68b5d9ed 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PluginCategoryTitle.kt @@ -2,7 +2,6 @@ package app.aaps.core.ui.compose import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings -import androidx.annotation.StringRes import app.aaps.core.data.plugin.PluginType /** diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/PlusMinusEdit.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt similarity index 88% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt index 0944adb85286..7777f28f404b 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SelectableListToolbar.kt @@ -21,8 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.ui.R +import app.aaps.core.interfaces.resources.TextResolver /** * Reusable toolbar builder for screens with selectable list items. @@ -50,7 +49,7 @@ fun SelectableListToolbar( onExitRemovingMode: () -> Unit, onNavigateBack: () -> Unit, onDelete: () -> Unit, - rh: ResourceHelper, + rh: TextResolver, title: String = "", showInvalidated: Boolean? = null, onToggleInvalidated: (() -> Unit)? = null, @@ -62,12 +61,12 @@ fun SelectableListToolbar( return if (isRemovingMode) { // Selection mode: show count, close icon, and delete action ToolbarConfig( - title = rh.gs(R.string.count_selected, selectedCount), + title = rh.gs(UiStrings.count_selected, selectedCount), navigationIcon = { IconButton(onClick = onExitRemovingMode) { Icon( imageVector = Icons.Default.Close, - contentDescription = rh.gs(R.string.close) + contentDescription = rh.gs(UiStrings.close) ) } }, @@ -76,7 +75,7 @@ fun SelectableListToolbar( IconButton(onClick = onDelete) { Icon( imageVector = Icons.Default.Delete, - contentDescription = rh.gs(R.string.delete), + contentDescription = rh.gs(UiStrings.delete), tint = MaterialTheme.colorScheme.error ) } @@ -90,7 +89,7 @@ fun SelectableListToolbar( IconButton(onClick = onNavigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = rh.gs(R.string.back) + contentDescription = rh.gs(UiStrings.back) ) } }, @@ -101,9 +100,9 @@ fun SelectableListToolbar( Icon( imageVector = if (showInvalidated) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = if (showInvalidated) - rh.gs(R.string.hide_invalidated) + rh.gs(UiStrings.hide_invalidated) else - rh.gs(R.string.show_invalidated) + rh.gs(UiStrings.show_invalidated) ) } } @@ -113,7 +112,7 @@ fun SelectableListToolbar( IconButton(onClick = onToggleLoop) { Icon( imageVector = if (showLoop) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = rh.gs(R.string.show_hide_records) + contentDescription = rh.gs(UiStrings.show_hide_records) ) } } @@ -128,7 +127,7 @@ fun SelectableListToolbar( IconButton(onClick = onSettings) { Icon( imageVector = Icons.Default.Settings, - contentDescription = rh.gs(R.string.nav_plugin_preferences) + contentDescription = rh.gs(UiStrings.nav_plugin_preferences) ) } } @@ -143,7 +142,7 @@ fun SelectableListToolbar( @Composable private fun MenuDropdown( menuItems: List, - rh: ResourceHelper + rh: TextResolver ) { var showMenu by remember { mutableStateOf(false) } @@ -151,7 +150,7 @@ private fun MenuDropdown( IconButton(onClick = { showMenu = true }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = rh.gs(R.string.more_options) + contentDescription = rh.gs(UiStrings.more_options) ) } DropdownMenu( diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SliderWithButtons.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/SliderWithButtonsPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/StatusLevel.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/TimeRangePicker.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ConfirmationMessage.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalDialogHost.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/GlobalSnackbarHost.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialog.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ValueInputDialogPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt index 5ba8b62ddab6..d830f45d10c0 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/ConcentrationDropDown.kt @@ -1,6 +1,5 @@ package app.aaps.core.ui.compose.insulin -import androidx.compose.ui.res.stringResource import app.aaps.core.ui.compose.stringResource import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.fillMaxWidth diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulin.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/insulin/SelectInsulinPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/HourWheelPicker.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveDoublePreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveListPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveMasterPasswordPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePasswordPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveStringPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveSwitchPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ClickablePreferenceCategoryHeader.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContent.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/CollapsibleCardSectionContentPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/InlinePreferenceItems.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ListPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/ListPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/Preference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceAlertDialog.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategory.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceCategoryPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt similarity index 98% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt index 8598c29c5c20..e9a971c34ab1 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceState.kt @@ -12,6 +12,8 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateMap +import app.aaps.core.data.format.NumberFormat +import app.aaps.core.data.format.NumberFormatPlatform import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.keys.interfaces.BooleanNonPreferenceKey import app.aaps.core.keys.interfaces.BooleanPreferenceKey @@ -33,8 +35,6 @@ import app.aaps.core.ui.compose.LocalProfileUtil import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch -import java.math.BigDecimal -import java.math.RoundingMode /** * Data class holding visibility and enabled state for a preference @@ -556,7 +556,8 @@ fun rememberUnitDoublePreferenceState( val displayValue = profileUtil.valueInCurrentUnitsDetect(storedValue) val isMgdl = displayValue == storedValue || (storedValue > 0 && displayValue / storedValue > 0.9) val precision = if (isMgdl) 0 else 1 - return BigDecimal(displayValue).setScale(precision, RoundingMode.HALF_UP).toPlainString() + // SEPARATOR_DOT, not the locale separator: this string is parsed back as a number when edited. + return NumberFormat.withDecimalsHalfUp(precision).format(displayValue, NumberFormatPlatform.SEPARATOR_DOT) } // Back the display value with the shared state map so it's reactive diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceTheme.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreviewUtils.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SwitchPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/SyncBadge.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreference.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/TextFieldPreferencePreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt similarity index 89% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt index f9eabfec6992..d9213e5d7ee5 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpCommunicationStatus.kt @@ -1,7 +1,8 @@ package app.aaps.core.ui.compose.pump +import kotlin.time.Clock import androidx.compose.ui.text.AnnotatedString -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventPumpStatusChanged @@ -24,7 +25,7 @@ import kotlinx.coroutines.flow.onEach class PumpCommunicationStatus( rxBus: RxBus, private val commandQueue: CommandQueue, - private val rh: ResourceHelper, + private val rh: TextResolver, scope: CoroutineScope ) { @@ -42,14 +43,14 @@ class PumpCommunicationStatus( .onEach { event -> val text = rh.gs(event.getStatus()) _statusBanner.value = if (text.isEmpty()) null else StatusBanner(text = text, level = StatusLevel.UNSPECIFIED) - refreshTrigger.value = System.currentTimeMillis() + refreshTrigger.value = Clock.System.now().toEpochMilliseconds() } .launchIn(scope) rxBus.toFlow(EventQueueChanged::class) .onEach { _queueStatus.value = commandQueue.statusAsAnnotated().takeIf { it.isNotEmpty() } - refreshTrigger.value = System.currentTimeMillis() + refreshTrigger.value = Clock.System.now().toEpochMilliseconds() } .launchIn(scope) } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewModels.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewScreen.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt similarity index 82% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt index c7d0eaaa11cb..1079941027a9 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/PumpOverviewStateBuilder.kt @@ -1,8 +1,7 @@ package app.aaps.core.ui.compose.pump import app.aaps.core.ui.UiStrings -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.ui.R +import app.aaps.core.interfaces.resources.TextResolver /** * Builds the common [PumpInfoRow] items that every pump shares. @@ -13,7 +12,7 @@ import app.aaps.core.ui.R * visibility logic. */ class PumpOverviewStateBuilder( - private val rh: ResourceHelper + private val rh: TextResolver ) { /** @@ -42,7 +41,7 @@ class PumpOverviewStateBuilder( if (lastConnection.isNotEmpty()) { add( PumpInfoRow( - label = rh.gs(R.string.last_connection_label), + label = rh.gs(UiStrings.last_connection_label), value = lastConnection ) ) @@ -52,7 +51,7 @@ class PumpOverviewStateBuilder( lastBolus?.let { add( PumpInfoRow( - label = rh.gs(R.string.last_bolus_label), + label = rh.gs(UiStrings.last_bolus_label), value = it ) ) @@ -62,7 +61,7 @@ class PumpOverviewStateBuilder( baseBasalRate?.let { add( PumpInfoRow( - label = rh.gs(R.string.base_basal_rate_label), + label = rh.gs(UiStrings.base_basal_rate_label), value = it ) ) @@ -71,7 +70,7 @@ class PumpOverviewStateBuilder( // Temp basal add( PumpInfoRow( - label = rh.gs(R.string.tempbasal_label), + label = rh.gs(UiStrings.tempbasal_label), value = tempBasalText, visible = tempBasalText.isNotEmpty() ) @@ -80,7 +79,7 @@ class PumpOverviewStateBuilder( // Extended bolus add( PumpInfoRow( - label = rh.gs(R.string.extended_bolus_label), + label = rh.gs(UiStrings.extended_bolus_label), value = extendedBolusText, visible = extendedBolusText.isNotEmpty() ) @@ -90,7 +89,7 @@ class PumpOverviewStateBuilder( battery?.let { add( PumpInfoRow( - label = rh.gs(R.string.battery_label), + label = rh.gs(UiStrings.battery_label), value = it ) ) @@ -100,7 +99,7 @@ class PumpOverviewStateBuilder( reservoir?.let { add( PumpInfoRow( - label = rh.gs(R.string.reservoir_label), + label = rh.gs(UiStrings.reservoir_label), value = it ) ) @@ -110,7 +109,7 @@ class PumpOverviewStateBuilder( serialNumber?.takeIf { it.isNotEmpty() }?.let { add( PumpInfoRow( - label = rh.gs(R.string.serial_number), + label = rh.gs(UiStrings.serial_number), value = it ) ) diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt similarity index 97% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt index 4a1c5a55c7c2..da75d5ac583d 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pump/WizardScreen.kt @@ -1,6 +1,7 @@ package app.aaps.core.ui.compose.pump -import androidx.activity.compose.BackHandler +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -46,6 +47,7 @@ import app.aaps.core.ui.compose.dialogs.OkCancelDialog * @param setToolbarConfig Callback to configure the parent toolbar (hides back arrow) * @param stepContent Composable content for the current step */ +@OptIn(ExperimentalComposeUiApi::class) @Composable fun WizardScreen( currentStep: S?, diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryList.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteEntryListPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPicker.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreen.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationPickerScreenPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStep.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/siteRotation/SiteLocationWizardStepPreviews.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt similarity index 78% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt index e07c0b21f09f..0e401fda1d4a 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt @@ -1,9 +1,9 @@ package app.aaps.core.ui.extensions import app.aaps.core.data.iob.CobInfo -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.ui.R +import app.aaps.core.ui.UiStrings /** * Text for a [CobInfo] on screen. @@ -22,9 +22,9 @@ fun CobInfo.generateCOBString(decimalFormatter: DecimalFormatter): String { return cobStringResult } -fun CobInfo.displayText(rh: ResourceHelper, decimalFormatter: DecimalFormatter): String? = +fun CobInfo.displayText(rh: TextResolver, decimalFormatter: DecimalFormatter): String? = displayCob?.let { displayCob -> - var cobText = rh.gs(R.string.format_carbs, displayCob.toInt()) + var cobText = rh.gs(UiStrings.format_carbs, displayCob.toInt()) if (futureCarbs > 0) cobText += "(" + decimalFormatter.to0Decimal(futureCarbs) + ")" cobText } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt similarity index 79% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt index e6dda5026c39..c8416b5030ba 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryTargetDisplay.kt @@ -4,9 +4,9 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.GlucoseUnit import app.aaps.core.data.model.TT import app.aaps.core.interfaces.profile.ProfileUtil -import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.ui.R +import app.aaps.core.ui.UiStrings import kotlin.time.Duration.Companion.milliseconds /** @@ -23,7 +23,7 @@ fun TT.highValueToUnitsToString(units: GlucoseUnit, decimalFormatter: DecimalFor if (units == GlucoseUnit.MGDL) decimalFormatter.to0Decimal(this.highTarget) else decimalFormatter.to1Decimal(this.highTarget * Constants.MGDL_TO_MMOLL) -fun TT.friendlyDescription(units: GlucoseUnit, rh: ResourceHelper, profileUtil: ProfileUtil): String = +fun TT.friendlyDescription(units: GlucoseUnit, rh: TextResolver, profileUtil: ProfileUtil): String = profileUtil.toTargetRangeString(lowTarget, highTarget, GlucoseUnit.MGDL, units) + profileUtil.unitLabel + - "@" + rh.gs(R.string.format_mins, duration.milliseconds.inWholeMinutes) + "(" + reason.text + ")" + "@" + rh.gs(UiStrings.format_mins, duration.milliseconds.inWholeMinutes) + "(" + reason.text + ")" diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/search/SearchableItem.kt diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt rename to core/ui/src/commonMain/kotlin/app/aaps/core/ui/search/SearchableProvider.kt diff --git a/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.ios.kt b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.ios.kt new file mode 100644 index 000000000000..07f7bd90b9bb --- /dev/null +++ b/core/ui/src/iosMain/kotlin/app/aaps/core/ui/compose/PlatformTheme.ios.kt @@ -0,0 +1,21 @@ +package app.aaps.core.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import kotlin.math.min + +/** iOS styles its status bar through the view controller, not from inside the composition. */ +@Composable +actual fun SystemBarAppearance(isDark: Boolean) = Unit + +/** + * Taken from the window rather than the device, which is the closest iOS equivalent. An iPad in + * split view therefore reports the pane it actually has, which is the number a layout wants anyway. + */ +@Composable +actual fun smallestScreenWidthDp(): Int { + val size = LocalWindowInfo.current.containerSize + val density = LocalDensity.current + return with(density) { min(size.width, size.height).toDp().value.toInt() } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08b8c49c6f29..61e3b9a15c04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -235,6 +235,7 @@ androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidge cmp-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } cmp-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } cmp-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +cmp-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "composeMultiplatform" } cmp-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "cmpMaterial3" } cmp-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "cmpIcons" } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt index e7c6639dd451..e388ed182d1f 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/resources/ResourceHelperImpl.kt @@ -5,6 +5,8 @@ import android.content.res.Configuration import androidx.annotation.PluralsRes import androidx.annotation.StringRes import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.resources.TextRefIdRegistry +import app.aaps.core.ui.UiStringIds import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences @@ -31,6 +33,13 @@ class ResourceHelperImpl @Inject constructor(var context: Context, private val f private var localizedContext: Context = buildLocalizedContext() init { + // Teach ResourceHelper the names :core:ui owns. It cannot see UiStringIds itself - :core:ui + // depends on :core:interfaces, not the other way round - so without this a `ui` name read + // outside a Composable renders as the raw name ("format_carbs" instead of "12 g"). + // Here rather than in :core:ui, because this class is downstream of every module that owns + // strings and is built before anything can ask it for text. + TextRefIdRegistry.register("ui") { name -> UiStringIds.idOf(name) } + // GeneralLanguage changes trigger Activity.recreate() which rebuilds the context // via attachBaseContext/LocaleHelper.wrap — no need to rebuild here and race on Main. preferences.observe(BooleanKey.GeneralSimpleMode).drop(1).onEach { diff --git a/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceScreenTest.kt b/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceScreenTest.kt index ec8da2f3c4e9..cfd58c1d1fa9 100644 --- a/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceScreenTest.kt +++ b/plugins/source/src/test/kotlin/app/aaps/plugins/source/compose/BgSourceScreenTest.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText +import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.data.model.GV import app.aaps.core.data.model.IDs import app.aaps.core.data.model.SourceSensor @@ -65,9 +66,10 @@ class BgSourceScreenTest { whenever(viewModel.rh).thenReturn(rh) whenever(viewModel.dateUtil).thenReturn(dateUtil) whenever(viewModel.formatGlucoseValue(any())).thenReturn("100 mg/dl") - // removing-mode toolbar builds its title via rh.gs(count_selected, n) and rh.gs(close) - whenever(rh.gs(any())).thenReturn("label") - whenever(rh.gs(any(), any())).thenReturn("selected") + // removing-mode toolbar builds its title via rh.gs(count_selected, n) and rh.gs(close). + // SelectableListToolbar names its strings with TextRef now, so stub that overload. + whenever(rh.gs(any())).thenReturn("label") + whenever(rh.gs(any(), any())).thenReturn("selected") whenever(prefs.observe(StringKey.GeneralDarkMode)).thenReturn(MutableStateFlow("light")) whenever(dateUtil.dateString(any())).thenReturn("2024-01-01") whenever(dateUtil.dateStringRelative(any(), any())).thenReturn("Today") diff --git a/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt b/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt index fef21ac0eebb..7ad15c263f60 100644 --- a/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt +++ b/pump/eopatch/src/test/kotlin/app/aaps/pump/eopatch/compose/EopatchOverviewViewModelTest.kt @@ -1,6 +1,7 @@ package app.aaps.pump.eopatch.compose import android.content.Context +import app.aaps.core.ui.UiStrings import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.pump.PumpSync @@ -102,11 +103,17 @@ internal class EopatchOverviewViewModelTest { // Info-row / action / banner labels touched by buildUiState (unstubbed -> null -> NPE). whenever(rh.gs(CoreUiR.string.tempbasal_label)).thenReturn("Temp basal") + whenever(rh.gs(UiStrings.tempbasal_label)).thenReturn("Temp basal") whenever(rh.gs(CoreUiR.string.extended_bolus_label)).thenReturn("Extended bolus") + whenever(rh.gs(UiStrings.extended_bolus_label)).thenReturn("Extended bolus") whenever(rh.gs(CoreUiR.string.status)).thenReturn("Status") + whenever(rh.gs(UiStrings.status)).thenReturn("Status") whenever(rh.gs(CoreUiR.string.reservoir_label)).thenReturn("Reservoir") + whenever(rh.gs(UiStrings.reservoir_label)).thenReturn("Reservoir") whenever(rh.gs(CoreUiR.string.pump_suspend)).thenReturn("Suspend") + whenever(rh.gs(UiStrings.pump_suspend)).thenReturn("Suspend") whenever(rh.gs(CoreUiR.string.pump_resume)).thenReturn("Resume") + whenever(rh.gs(UiStrings.pump_resume)).thenReturn("Resume") whenever(rh.gs(R.string.eopatch_not_activated)).thenReturn("Not activated") whenever(rh.gs(R.string.string_activate_patch)).thenReturn("Activate Patch") whenever(rh.gs(R.string.string_running)).thenReturn("Running") diff --git a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt index aab5322bb346..2bcd6a969d25 100644 --- a/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt +++ b/pump/medtrum/src/test/kotlin/app/aaps/pump/medtrum/compose/MedtrumOverviewViewModelTest.kt @@ -1,6 +1,7 @@ package app.aaps.pump.medtrum.compose import android.content.Context +import app.aaps.core.ui.UiStrings import app.aaps.core.interfaces.insulin.ConcentrationHelper import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.UserEntryLogger @@ -94,11 +95,15 @@ internal class MedtrumOverviewViewModelTest { // Every info-row / action / banner label the build path touches (unstubbed rh.gs -> null -> NPE in PumpInfoRow) // Common rows (core:ui builder) whenever(rh.gs(CoreUiR.string.tempbasal_label)).thenReturn("Temp basal") + whenever(rh.gs(UiStrings.tempbasal_label)).thenReturn("Temp basal") whenever(rh.gs(CoreUiR.string.extended_bolus_label)).thenReturn("Extended bolus") + whenever(rh.gs(UiStrings.extended_bolus_label)).thenReturn("Extended bolus") whenever(rh.gs(CoreUiR.string.serial_number)).thenReturn("Serial number") + whenever(rh.gs(UiStrings.serial_number)).thenReturn("Serial number") // Medtrum-specific rows whenever(rh.gs(R.string.pump_state_label)).thenReturn("Pump state") whenever(rh.gs(CoreUiR.string.base_basal_rate_label)).thenReturn("Base basal") + whenever(rh.gs(UiStrings.base_basal_rate_label)).thenReturn("Base basal") whenever(rh.gs(R.string.pump_type_label)).thenReturn("Pump type") whenever(rh.gs(R.string.patch_no_label)).thenReturn("Patch no") whenever(rh.gs(R.string.expiry_not_enabled)).thenReturn("") // empty -> expiry row skipped @@ -109,9 +114,11 @@ internal class MedtrumOverviewViewModelTest { whenever(rh.gs(R.string.patch_not_active)).thenReturn("Patch not activated") // Actions whenever(rh.gs(CoreUiR.string.refresh)).thenReturn("Refresh") + whenever(rh.gs(UiStrings.refresh)).thenReturn("Refresh") whenever(rh.gs(R.string.reset_alarms_label)).thenReturn("Reset alarms") whenever(rh.gs(R.string.change_patch_label)).thenReturn("Change patch") whenever(rh.gs(CoreUiR.string.pump_unpair)).thenReturn("Unpair") + whenever(rh.gs(UiStrings.pump_unpair)).thenReturn("Unpair") } private fun createViewModel() = MedtrumOverviewViewModel( From 358fa858783c8b80aca31903ff22d80b68c40ee9 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 16:52:32 +0200 Subject: [PATCH 119/146] Eight :core:interfaces files were in the wrong source set Loop, ConfigBuilder, ConstraintsChecker, PluginConstraints, ProfileFunction, TemporaryBasalStorage, CalculationSignals and CalculationWorkflow import nothing from Android, Java, RxJava or Dagger. They were in androidMain only because that is where the module started, so moving them is a no-op for every caller. Found by listing androidMain files with no platform import at all - the same check that turned up ProfileUtil. Five other candidates from that list did NOT move and stay put: Maintenance, Prefs and PrefsFile need ExportResult and PrefsMetadataKey, SmsCommunicator needs Sms, and MidnightTime uses synchronized. 233 commonMain / 29 androidMain. --- .../kotlin/app/aaps/core/interfaces/aps/Loop.kt | 0 .../app/aaps/core/interfaces/configuration/ConfigBuilder.kt | 0 .../app/aaps/core/interfaces/constraints/ConstraintsChecker.kt | 0 .../app/aaps/core/interfaces/constraints/PluginConstraints.kt | 0 .../kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt | 0 .../kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt | 0 .../app/aaps/core/interfaces/workflow/CalculationSignals.kt | 0 .../app/aaps/core/interfaces/workflow/CalculationWorkflow.kt | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/aps/Loop.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt (100%) rename core/interfaces/src/{androidMain => commonMain}/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt (100%) diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/aps/Loop.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/configuration/ConfigBuilder.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/ConstraintsChecker.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/constraints/PluginConstraints.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/profile/ProfileFunction.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/pump/TemporaryBasalStorage.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/workflow/CalculationSignals.kt diff --git a/core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt b/core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt similarity index 100% rename from core/interfaces/src/androidMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt rename to core/interfaces/src/commonMain/kotlin/app/aaps/core/interfaces/workflow/CalculationWorkflow.kt From 6f3493f48d47200443e19c0b5f5b343d660c6ce6 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 17:08:48 +0200 Subject: [PATCH 120/146] Split display out of the TB and EB extensions :core:objects goes from 5 files needing :core:ui down to 3. The cycle that made this awkward is gone: the formatting now lives in :core:ui, and :core:ui still has zero references to :core:objects. The blocker was never the formatting itself, it was three helpers it called - TB.getPassedDurationToTimeInMinutes, EB.getPassedDurationToTimeInMinutes and TB.durationInMinutes. They are used across plugins/aps, implementation and the androidTest adapters, so they could not simply travel with the formatting, and while they sat in :core:objects nothing in :core:ui could call them. They read nothing but the record's own timestamp and duration, so they moved DOWN to :core:data instead, next to the models. With them there, TB.toStringFull / toStringShort and EB.toStringFull / toStringMedium moved up to :core:ui with netExtendedRate, and take TextResolver rather than ResourceHelper. durationInMinutes stays Long. T.mins() returns Long and AutotuneIob assigns it straight to a val, so narrowing it to Int would have changed a caller's type. TizenPluginTest needed the gs(TextRef, vararg) overload stubbed as well as gs(TextRef). Mockito does not run default interface methods, so the vararg form answered null, toStringFull produced null and the bundle key vanished - the test failed on a missing key rather than on wrong text, which is a slow way to find out. Still holding :core:objects to :core:ui: ProfileSealed, BolusWizard and RunningModeGuard. Those are domain classes choosing display strings, so they need the strings relocated or the text moved out - not a file move. --- .../openAPSAMA/DetermineBasalAdapterAMAJS.kt | 2 +- .../openAPSSMB/DetermineBasalAdapterSMBJS.kt | 2 +- .../DetermineBasalAdapterAutoISFJS.kt | 2 +- .../DetermineBasalAdapterSMBDynamicISFJS.kt | 2 +- .../core/data/model/DurationExtensions.kt | 26 ++++++++++ .../extensions/ExtendedBolusExtension.kt | 13 +---- .../extensions/TemporaryBasalExtension.kt | 48 +------------------ .../ui/extensions/ExtendedBolusDisplay.kt | 23 +++++++++ .../ui/extensions/TemporaryBasalDisplay.kt | 46 ++++++++++++++++++ .../aaps/plugins/aps/autotune/AutotuneIob.kt | 2 +- .../aps/openAPSAMA/OpenAPSAMAPlugin.kt | 2 +- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 2 +- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 2 +- .../PersistentNotificationPlugin.kt | 2 +- .../aaps/plugins/sync/tizen/TizenPlugin.kt | 4 +- .../wear/wearintegration/DataHandlerMobile.kt | 2 +- .../aaps/plugins/sync/xdrip/XdripPlugin.kt | 2 +- .../plugins/sync/tizen/TizenPluginTest.kt | 9 ++++ .../app/aaps/ui/compose/main/MainViewModel.kt | 2 +- .../ui/compose/manageSheet/ManageViewModel.kt | 4 +- .../viewmodels/TempBasalViewModel.kt | 2 +- 21 files changed, 123 insertions(+), 76 deletions(-) create mode 100644 core/data/src/commonMain/kotlin/app/aaps/core/data/model/DurationExtensions.kt create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/ExtendedBolusDisplay.kt create mode 100644 core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryBasalDisplay.kt diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt index 9747831200c8..942f9b609163 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/DetermineBasalAdapterAMAJS.kt @@ -19,7 +19,7 @@ import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertToJSONArray import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.plugins.aps.logger.LoggerCallback import app.aaps.plugins.aps.utils.ScriptReader diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt index ba54ed77d934..a5eca01a05b0 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/DetermineBasalAdapterSMBJS.kt @@ -22,7 +22,7 @@ import app.aaps.core.keys.IntKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertToJSONArray import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.plugins.aps.logger.LoggerCallback import app.aaps.plugins.aps.utils.ScriptReader diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBAutoISF/DetermineBasalAdapterAutoISFJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBAutoISF/DetermineBasalAdapterAutoISFJS.kt index b4ae4d7795d9..ece5f7bb0cc2 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBAutoISF/DetermineBasalAdapterAutoISFJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBAutoISF/DetermineBasalAdapterAutoISFJS.kt @@ -24,7 +24,7 @@ import app.aaps.core.keys.IntKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertToJSONArray import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.core.objects.profile.ProfileSealed import app.aaps.plugins.aps.logger.LoggerCallback diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt index 41d7bae68f06..dd5a6e276f82 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMBDynamicISF/DetermineBasalAdapterSMBDynamicISFJS.kt @@ -23,7 +23,7 @@ import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertToJSONArray import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.plugins.aps.logger.LoggerCallback import app.aaps.plugins.aps.openAPSSMB.DetermineBasalResultSMBFromJS diff --git a/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DurationExtensions.kt b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DurationExtensions.kt new file mode 100644 index 000000000000..fe7f28177f98 --- /dev/null +++ b/core/data/src/commonMain/kotlin/app/aaps/core/data/model/DurationExtensions.kt @@ -0,0 +1,26 @@ +package app.aaps.core.data.model + +import app.aaps.core.data.time.T +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * How long a temporary basal or extended bolus has been running, and how long it was meant to run. + * + * These live next to the models rather than in `:core:objects` because they read nothing but the + * record's own timestamp and duration. That matters beyond tidiness: `:core:objects` depends on + * `:core:ui`, so anything that stayed there could not be used from `:core:ui` without a dependency + * cycle - which is exactly what kept the `toStringFull` family from moving to the layer that draws. + */ + +/** Minutes elapsed between the start and [time], never counting past the end. */ +fun TB.getPassedDurationToTimeInMinutes(time: Long): Int = + ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() + +/** Minutes elapsed between the start and [time], never counting past the end. */ +fun EB.getPassedDurationToTimeInMinutes(time: Long): Int = + ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() + +/** The full planned length, in minutes. */ +val TB.durationInMinutes: Long + get() = T.msecs(duration).mins() diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ExtendedBolusExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ExtendedBolusExtension.kt index aca9323dc154..6d59413da963 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ExtendedBolusExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/ExtendedBolusExtension.kt @@ -4,6 +4,7 @@ import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.BS import app.aaps.core.data.model.EB import app.aaps.core.data.model.TB +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.data.time.T import app.aaps.core.interfaces.aps.AutosensResult import app.aaps.core.interfaces.aps.IobTotal @@ -25,18 +26,6 @@ fun EB.isInProgress(dateUtil: DateUtil): Boolean = val EB.plannedRemainingMinutes: Int get() = max(round((end - System.currentTimeMillis()) / 1000.0 / 60).toInt(), 0) -fun EB.toStringFull(dateUtil: DateUtil, rh:ResourceHelper): String = - rh.gs(app.aaps.core.ui.R.string.extended_bolus_full, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) - -fun EB.toStringFull(dateUtil: DateUtil, ch: ConcentrationHelper): String = - "${ch.basalRateString(PumpRate(rate), true)} ${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${T.msecs(duration).mins()}" - -fun EB.toStringMedium(dateUtil: DateUtil, rh:ResourceHelper): String = - rh.gs(app.aaps.core.ui.R.string.extended_bolus_medium, rate, getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) - -fun EB.getPassedDurationToTimeInMinutes(time: Long): Int = - ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() - fun EB.toTemporaryBasal(profile: Profile): TB = TB( timestamp = timestamp, diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt index 45b5200f4d25..b5cff4b797b6 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/extensions/TemporaryBasalExtension.kt @@ -3,6 +3,7 @@ package app.aaps.core.objects.extensions import app.aaps.core.data.configuration.Constants import app.aaps.core.data.model.BS import app.aaps.core.data.model.TB +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.data.time.T import app.aaps.core.interfaces.aps.AutosensResult import app.aaps.core.interfaces.aps.IobTotal @@ -29,9 +30,6 @@ import kotlin.math.roundToInt fun TB.isInProgress(dateUtil: DateUtil): Boolean = dateUtil.now() in timestamp..timestamp + duration -fun TB.getPassedDurationToTimeInMinutes(time: Long): Int = - ((min(time, end) - timestamp) / 60.0 / 1000).roundToInt() - val TB.plannedRemainingMinutes: Int get() = max(round((end - System.currentTimeMillis()) / 1000.0 / 60).toInt(), 0) @@ -43,50 +41,6 @@ fun TB.convertedToPercent(time: Long, profile: Profile): Int = if (!isAbsolute) rate.toInt() else (rate / profile.getBasal(time) * 100).toInt() -private fun TB.netExtendedRate(profile: Profile) = rate - profile.getBasal(timestamp) -val TB.durationInMinutes - get() = T.msecs(duration).mins() - -fun TB.toStringFull(profile: Profile, dateUtil: DateUtil, rh: ResourceHelper): String { - val timeAndDuration = "${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${durationInMinutes}'" - - return when { - type == TB.Type.FAKE_EXTENDED -> { - rh.gs(app.aaps.core.ui.R.string.temp_basal_tsf_fake_extended, rate, netExtendedRate(profile), timeAndDuration) - } - - isAbsolute -> { - rh.gs(app.aaps.core.ui.R.string.temp_basal_tsf_absolute, rate, timeAndDuration) - } - - else -> { // percent - rh.gs(app.aaps.core.ui.R.string.temp_basal_tsf_percent, rate, timeAndDuration) - } - } -} - -fun TB.toStringFull(profile: Profile, dateUtil: DateUtil, ch: ConcentrationHelper): String { - val timeAndDuration = "${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${durationInMinutes}'" - - return when { - type == TB.Type.FAKE_EXTENDED -> { - "${ch.basalRateString(PumpRate(rate), true)} (${netExtendedRate(profile)}E) $timeAndDuration" - } - - isAbsolute -> { - "${ch.basalRateString(PumpRate(rate), true)} $timeAndDuration" - } - - else -> { // percent - "${ch.basalRateString(PumpRate(rate), false)} $timeAndDuration" - } - } -} - -fun TB.toStringShort(rh: ResourceHelper): String = - if (isAbsolute || type == TB.Type.FAKE_EXTENDED) rh.gs(app.aaps.core.ui.R.string.pump_base_basal_rate, rate) - else rh.gs(app.aaps.core.ui.R.string.formatPercent, rate) - fun TB.iobCalc(time: Long, profile: EffectiveProfile): IobTotal { if (!isValid) return IobTotal(time) val result = IobTotal(time) diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/ExtendedBolusDisplay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/ExtendedBolusDisplay.kt new file mode 100644 index 000000000000..836b00486b35 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/ExtendedBolusDisplay.kt @@ -0,0 +1,23 @@ +package app.aaps.core.ui.extensions + +import app.aaps.core.data.model.EB +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes +import app.aaps.core.data.time.T +import app.aaps.core.interfaces.insulin.ConcentrationHelper +import app.aaps.core.interfaces.pump.PumpRate +import app.aaps.core.interfaces.resources.TextResolver +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.ui.UiStrings + +/** + * Text for an extended bolus on screen. The record's own maths stays in `:core:objects` - see + * [TB.toStringFull] for why the split runs where it does. + */ +fun EB.toStringFull(dateUtil: DateUtil, rh: TextResolver): String = + rh.gs(UiStrings.extended_bolus_full, rate, dateUtil.timeString(timestamp), getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) + +fun EB.toStringFull(dateUtil: DateUtil, ch: ConcentrationHelper): String = + "${ch.basalRateString(PumpRate(rate), true)} ${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${T.msecs(duration).mins()}" + +fun EB.toStringMedium(dateUtil: DateUtil, rh: TextResolver): String = + rh.gs(UiStrings.extended_bolus_medium, rate, getPassedDurationToTimeInMinutes(dateUtil.now()), T.msecs(duration).mins()) diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryBasalDisplay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryBasalDisplay.kt new file mode 100644 index 000000000000..dd88543c3b60 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/TemporaryBasalDisplay.kt @@ -0,0 +1,46 @@ +package app.aaps.core.ui.extensions + +import app.aaps.core.data.model.TB +import app.aaps.core.data.model.durationInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes +import app.aaps.core.interfaces.insulin.ConcentrationHelper +import app.aaps.core.interfaces.profile.Profile +import app.aaps.core.interfaces.pump.PumpRate +import app.aaps.core.interfaces.resources.TextResolver +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.ui.UiStrings + +/** + * Text for a temporary basal on screen. + * + * The maths that belongs to the record - `iobCalc`, `convertedToAbsolute`, `convertedToPercent` - + * stays in `:core:objects`. Only the part that formats for a reader lives here, next to the strings + * it uses. What made that possible was moving the two time helpers down to `:core:data`: while they + * sat in `:core:objects`, using them from here would have meant `:core:ui` depending on + * `:core:objects`, and that module still depends on this one. + */ +private fun TB.netExtendedRate(profile: Profile) = rate - profile.getBasal(timestamp) + +fun TB.toStringFull(profile: Profile, dateUtil: DateUtil, rh: TextResolver): String { + val timeAndDuration = "${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${durationInMinutes}'" + + return when { + type == TB.Type.FAKE_EXTENDED -> rh.gs(UiStrings.temp_basal_tsf_fake_extended, rate, netExtendedRate(profile), timeAndDuration) + isAbsolute -> rh.gs(UiStrings.temp_basal_tsf_absolute, rate, timeAndDuration) + else -> rh.gs(UiStrings.temp_basal_tsf_percent, rate, timeAndDuration) + } +} + +fun TB.toStringFull(profile: Profile, dateUtil: DateUtil, ch: ConcentrationHelper): String { + val timeAndDuration = "${dateUtil.timeString(timestamp)} ${getPassedDurationToTimeInMinutes(dateUtil.now())}/${durationInMinutes}'" + + return when { + type == TB.Type.FAKE_EXTENDED -> "${ch.basalRateString(PumpRate(rate), true)} (${netExtendedRate(profile)}E) $timeAndDuration" + isAbsolute -> "${ch.basalRateString(PumpRate(rate), true)} $timeAndDuration" + else -> "${ch.basalRateString(PumpRate(rate), false)} $timeAndDuration" + } +} + +fun TB.toStringShort(rh: TextResolver): String = + if (isAbsolute || type == TB.Type.FAKE_EXTENDED) rh.gs(UiStrings.pump_base_basal_rate, rate) + else rh.gs(UiStrings.formatPercent, rate) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotuneIob.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotuneIob.kt index f60fff805546..bb854ed4657d 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotuneIob.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/autotune/AutotuneIob.kt @@ -22,7 +22,7 @@ import app.aaps.core.interfaces.utils.Round import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.durationInMinutes +import app.aaps.core.data.model.durationInMinutes import app.aaps.core.objects.extensions.round import app.aaps.core.objects.extensions.toJson import app.aaps.core.objects.extensions.toTemporaryBasal diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt index d9e311b16947..e1707fcb7e3e 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAMA/OpenAPSAMAPlugin.kt @@ -38,7 +38,7 @@ import app.aaps.core.keys.DoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.core.objects.extensions.target import app.aaps.core.ui.compose.icons.IcPluginOpenAPS diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt index 529f79347469..fa68c7a311b2 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSAutoISF/OpenAPSAutoISFPlugin.kt @@ -49,7 +49,7 @@ import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.core.objects.extensions.target import app.aaps.core.objects.profile.ProfileSealed diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt index 8c0110bd0ffc..525be27f4e9b 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/openAPSSMB/OpenAPSSMBPlugin.kt @@ -51,7 +51,7 @@ import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.objects.constraints.ConstraintObject import app.aaps.core.objects.extensions.convertedToAbsolute -import app.aaps.core.objects.extensions.getPassedDurationToTimeInMinutes +import app.aaps.core.data.model.getPassedDurationToTimeInMinutes import app.aaps.core.objects.extensions.plannedRemainingMinutes import app.aaps.core.objects.extensions.target import app.aaps.core.objects.profile.ProfileSealed diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index e178e5230b2e..9d168a879b06 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -40,7 +40,7 @@ import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.objects.extensions.apsAdjustedTargetMgdl import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round -import app.aaps.core.objects.extensions.toStringShort +import app.aaps.core.ui.extensions.toStringShort import app.aaps.core.utils.DeferredForegroundStart import app.aaps.plugins.main.R import app.aaps.core.interfaces.rx.collectResilient diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 24beb4b5bc3f..fc2432215137 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -32,9 +32,9 @@ import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.UnitDoubleKey import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.objects.extensions.durationInMinutes +import app.aaps.core.data.model.durationInMinutes import app.aaps.core.objects.extensions.round -import app.aaps.core.objects.extensions.toStringFull +import app.aaps.core.ui.extensions.toStringFull import app.aaps.core.ui.compose.icons.IcPluginTizen import app.aaps.plugins.sync.R import app.aaps.shared.impl.extensions.safeQueryBroadcastReceivers diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index ba31551c2945..2026939d9081 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -87,7 +87,7 @@ import app.aaps.core.objects.extensions.apsAdjustedTargetMgdl import app.aaps.core.objects.extensions.convertedToAbsolute import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round -import app.aaps.core.objects.extensions.toStringShort +import app.aaps.core.ui.extensions.toStringShort import app.aaps.core.objects.extensions.valueToUnits import app.aaps.core.objects.runningMode.PumpCommandGate import app.aaps.core.objects.runningMode.RunningModeGuard diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt index 08beb3ec411d..1e86352ec9d4 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/xdrip/XdripPlugin.kt @@ -46,7 +46,7 @@ import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.ui.extensions.generateCOBString import app.aaps.core.objects.extensions.round -import app.aaps.core.objects.extensions.toStringShort +import app.aaps.core.ui.extensions.toStringShort import app.aaps.core.objects.profile.ProfileSealed import app.aaps.core.ui.compose.icons.IcXDrip import app.aaps.core.ui.compose.preference.PreferenceSubScreenDef diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt index 31fe4e5d1d7f..fc7dd7f78477 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt @@ -30,6 +30,7 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock import org.mockito.kotlin.any +import org.mockito.kotlin.anyVararg import org.mockito.kotlin.whenever internal class TizenPluginTest : TestBaseWithProfile() { @@ -58,6 +59,14 @@ internal class TizenPluginTest : TestBaseWithProfile() { is TextRef.AndroidRes -> "S" + ref.id } } + // Same for the formatting overload, which TB.toStringFull uses. + whenever(rh.gs(any(), anyVararg())).thenAnswer { + when (val ref = it.getArgument(0)) { + is TextRef.Literal -> ref.text + is TextRef.Named -> ref.name + is TextRef.AndroidRes -> "S" + ref.id + } + } whenever(iobCobCalculator.ads).thenReturn(autosensDataStore) whenever(autosensDataStore.lastBg()).thenReturn(InMemoryGlucoseValue(1000, 100.0, sourceSensor = SourceSensor.UNKNOWN)) runBlocking { whenever(profileFunction.getProfile()).thenReturn(effectiveProfile) } diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt index feb37b693b18..a5098742fe24 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/main/MainViewModel.kt @@ -57,7 +57,7 @@ import app.aaps.core.keys.StringNonKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.VisibilityContext import app.aaps.core.objects.constraints.ConstraintObject -import app.aaps.core.objects.extensions.toStringFull +import app.aaps.core.ui.extensions.toStringFull import app.aaps.core.objects.wizard.QuickWizard import app.aaps.core.objects.wizard.QuickWizardEntry import app.aaps.core.objects.wizard.QuickWizardMode diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt index 76a7294d38e4..95e62d475146 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/manageSheet/ManageViewModel.kt @@ -32,8 +32,8 @@ import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.keys.BooleanKey import app.aaps.core.keys.interfaces.Preferences import app.aaps.core.keys.interfaces.VisibilityContext -import app.aaps.core.objects.extensions.toStringMedium -import app.aaps.core.objects.extensions.toStringShort +import app.aaps.core.ui.extensions.toStringMedium +import app.aaps.core.ui.extensions.toStringShort import app.aaps.core.ui.R import app.aaps.core.interfaces.navigation.ElementType import app.aaps.ui.R as UiR diff --git a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModel.kt b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModel.kt index f14cf88b639f..24053bf0d2c5 100644 --- a/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModel.kt +++ b/ui/src/main/kotlin/app/aaps/ui/compose/treatments/viewmodels/TempBasalViewModel.kt @@ -20,7 +20,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventShowSnackbar import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.objects.extensions.toStringFull +import app.aaps.core.ui.extensions.toStringFull import app.aaps.core.objects.extensions.toTemporaryBasal import app.aaps.core.ui.R import app.aaps.core.ui.compose.SelectableListToolbar From 7b4adad4b42d30e31121cde6195e989d4d4e1b17 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 17 Aug 2026 19:10:15 +0200 Subject: [PATCH 121/146] Move 33 shared strings to :core:interfaces, freeing :core:objects from :core:ui :core:objects no longer depends on :core:ui at all. That edge was backwards - a domain module reaching into the UI module - and it is what blocked :core:objects, and through it :core:graph and the 282-file :ui, from becoming multiplatform. ## The strings ProfileSealed, BolusWizard and RunningModeGuard between them name 33 strings that lived in :core:ui: the value+unit templates (format_carbs, format_insulin_units, confirmation_line, mins), the profile validation messages (value_out_of_hard_limits, basalprofilenotaligned, profile_*) and the wizard's confirmation text. They moved to :core:interfaces, which every one of these modules already depends on and which already owns a strings.xml and an InterfacesStrings generator. No name collided. 795 elements moved - 33 names across the 24 locales that actually had a translation for them. The total element count across both modules is unchanged at 21000, and none of the 33 is left anywhere in :core:ui. The move was line-exact rather than XML-rewritten, so the `comment=` translator notes and every byte of the translated text survive untouched. ## The call sites 138 files. Four different shapes had to be handled, and only the first was obvious: - app.aaps.core.ui.R.string.X, fully qualified - UiStrings.X, the generated TextRef form - a bare R.string.X, from `import app.aaps.core.ui.R` - an aliased CoreUiR/UiR/CoreR.string.X For a file whose R import pointed only at moved strings the import was swapped. Where a file mixes moved and unmoved names - TranslatorImpl uses 4 moved out of 205 - it gained `import app.aaps.core.interfaces.R as InterfacesR` and only the moved references were rewritten. ## What this does not fix The three domain classes still choose display strings; they just choose them from a module below rather than above. Making BolusWizard return structured data instead of built text is still the right change, and is now a refactor that can be done on its own rather than a prerequisite for the module graph. --- .../aps/openAPSAMA/TestOpenAPSAMAPlugin.kt | 10 ++-- .../aps/openAPSSMB/TestOpenAPSSMBPlugin.kt | 10 ++-- .../profile/ProfileCompareRowBuilders.kt | 7 +-- .../androidMain/res/values-bg-rBG/strings.xml | 33 ++++++++++++ .../androidMain/res/values-ca-rES/strings.xml | 23 +++++++++ .../androidMain/res/values-cs-rCZ/strings.xml | 33 ++++++++++++ .../androidMain/res/values-da-rDK/strings.xml | 27 ++++++++++ .../androidMain/res/values-de-rDE/strings.xml | 27 ++++++++++ .../androidMain/res/values-el-rGR/strings.xml | 27 ++++++++++ .../androidMain/res/values-es-rES/strings.xml | 33 ++++++++++++ .../androidMain/res/values-fr-rFR/strings.xml | 33 ++++++++++++ .../androidMain/res/values-hr-rHR/strings.xml | 16 ++++++ .../androidMain/res/values-hu-rHU/strings.xml | 5 ++ .../androidMain/res/values-it-rIT/strings.xml | 33 ++++++++++++ .../androidMain/res/values-iw-rIL/strings.xml | 27 ++++++++++ .../androidMain/res/values-ko-rKR/strings.xml | 27 ++++++++++ .../androidMain/res/values-lt-rLT/strings.xml | 27 ++++++++++ .../androidMain/res/values-nb-rNO/strings.xml | 33 ++++++++++++ .../androidMain/res/values-nl-rNL/strings.xml | 28 ++++++++++ .../androidMain/res/values-pl-rPL/strings.xml | 28 ++++++++++ .../androidMain/res/values-pt-rBR/strings.xml | 26 ++++++++++ .../androidMain/res/values-pt-rPT/strings.xml | 25 +++++++++ .../androidMain/res/values-ro-rRO/strings.xml | 33 ++++++++++++ .../androidMain/res/values-ru-rRU/strings.xml | 28 ++++++++++ .../androidMain/res/values-sk-rSK/strings.xml | 33 ++++++++++++ .../androidMain/res/values-sr-rCS/strings.xml | 2 + .../androidMain/res/values-sv-rSE/strings.xml | 27 ++++++++++ .../androidMain/res/values-tr-rTR/strings.xml | 28 ++++++++++ .../androidMain/res/values-uk-rUA/strings.xml | 1 + .../androidMain/res/values-vi-rVN/strings.xml | 33 ++++++++++++ .../androidMain/res/values-zh-rCN/strings.xml | 33 ++++++++++++ .../androidMain/res/values-zh-rTW/strings.xml | 33 ++++++++++++ .../src/androidMain/res/values/strings.xml | 33 ++++++++++++ core/objects/build.gradle.kts | 1 - .../core/objects/profile/ProfileSealed.kt | 2 +- .../objects/runningMode/RunningModeGuard.kt | 2 +- .../aaps/core/objects/wizard/BolusWizard.kt | 44 ++++++++-------- .../core/objects/profile/ProfileSealedTest.kt | 8 +-- .../aaps/core/ui/compose/ProtectionHost.kt | 1 - .../preference/AdaptivePreferenceItem.kt | 1 - .../androidMain/res/values-bg-rBG/strings.xml | 33 ------------ .../androidMain/res/values-ca-rES/strings.xml | 23 --------- .../androidMain/res/values-cs-rCZ/strings.xml | 33 ------------ .../androidMain/res/values-da-rDK/strings.xml | 27 ---------- .../androidMain/res/values-de-rDE/strings.xml | 27 ---------- .../androidMain/res/values-el-rGR/strings.xml | 27 ---------- .../androidMain/res/values-es-rES/strings.xml | 33 ------------ .../androidMain/res/values-fr-rFR/strings.xml | 33 ------------ .../androidMain/res/values-hr-rHR/strings.xml | 16 ------ .../androidMain/res/values-hu-rHU/strings.xml | 5 -- .../androidMain/res/values-it-rIT/strings.xml | 33 ------------ .../androidMain/res/values-iw-rIL/strings.xml | 27 ---------- .../androidMain/res/values-ko-rKR/strings.xml | 27 ---------- .../androidMain/res/values-lt-rLT/strings.xml | 27 ---------- .../androidMain/res/values-nb-rNO/strings.xml | 33 ------------ .../androidMain/res/values-nl-rNL/strings.xml | 28 ---------- .../androidMain/res/values-pl-rPL/strings.xml | 28 ---------- .../androidMain/res/values-pt-rBR/strings.xml | 26 ---------- .../androidMain/res/values-pt-rPT/strings.xml | 25 --------- .../androidMain/res/values-ro-rRO/strings.xml | 33 ------------ .../androidMain/res/values-ru-rRU/strings.xml | 28 ---------- .../androidMain/res/values-sk-rSK/strings.xml | 33 ------------ .../androidMain/res/values-sr-rCS/strings.xml | 2 - .../androidMain/res/values-sv-rSE/strings.xml | 27 ---------- .../androidMain/res/values-tr-rTR/strings.xml | 28 ---------- .../androidMain/res/values-uk-rUA/strings.xml | 1 - .../androidMain/res/values-vi-rVN/strings.xml | 33 ------------ .../androidMain/res/values-zh-rCN/strings.xml | 33 ------------ .../androidMain/res/values-zh-rTW/strings.xml | 33 ------------ .../ui/src/androidMain/res/values/strings.xml | 33 ------------ .../core/ui/compose/NumberInputRowPreviews.kt | 3 +- .../dialogs/ElementConfirmationDialog.kt | 1 - .../ui/compose/navigation/ElementTypeStyle.kt | 3 +- .../ui/compose/pickers/WeekDaySelector.kt | 1 - .../preference/AdaptiveIntentPreference.kt | 1 - .../preference/PreferenceSliderWithButtons.kt | 1 - .../aaps/core/ui/extensions/CobInfoDisplay.kt | 4 +- .../bolus/WizardBolusExecutorImpl.kt | 51 ++++++++++--------- .../insulin/ConcentrationHelperImpl.kt | 2 +- .../pump/PumpStatusProviderImpl.kt | 2 +- .../queue/CommandQueueImplementation.kt | 6 +-- .../queue/commands/CommandBolus.kt | 4 +- .../queue/commands/CommandSMBBolus.kt | 2 +- .../implementation/scenes/SceneExecutor.kt | 5 +- .../scenes/SceneExpiryWorker.kt | 3 +- .../UserEntryPresentationHelperImpl.kt | 3 +- .../utils/DecimalFormatterImpl.kt | 2 +- .../implementation/utils/TranslatorImpl.kt | 9 ++-- .../queue/CommandQueueImplementationTest.kt | 8 +-- .../utils/HardLimitsImplTest.kt | 30 +++++------ .../aps/autotune/compose/AutotuneScreen.kt | 8 +-- .../app/aaps/plugins/aps/loop/LoopPlugin.kt | 14 ++--- .../aps/openAPSAMA/OpenAPSAMAPlugin.kt | 10 ++-- .../openAPSAutoISF/OpenAPSAutoISFPlugin.kt | 10 ++-- .../aps/openAPSSMB/OpenAPSSMBPlugin.kt | 10 ++-- .../plugins/automation/AutomationRuntime.kt | 2 +- .../smsCommunicator/SmsCommunicatorPlugin.kt | 4 +- .../actions/ExtendedSetAction.kt | 6 +-- .../wear/wearintegration/DataHandlerMobile.kt | 20 ++++---- .../SmsCommunicatorPluginTest.kt | 4 +- .../pump/dana/compose/DanaHistoryViewModel.kt | 6 +-- .../diaconn/compose/DiaconnHistoryScreen.kt | 2 +- .../compose/DiaconnHistoryViewModel.kt | 6 +-- .../compose/EopatchOverviewViewModel.kt | 5 +- .../CalibrationDialogViewModel.kt | 3 +- .../compose/carbsDialog/CarbsDialogScreen.kt | 4 +- .../carbsDialog/CarbsDialogViewModel.kt | 12 ++--- .../compose/careDialog/CareDialogViewModel.kt | 12 ++--- .../ExtendedBolusDialogScreen.kt | 3 +- .../compose/fillDialog/FillDialogViewModel.kt | 11 ++-- .../insulinDialog/InsulinDialogScreen.kt | 5 +- .../insulinDialog/InsulinDialogViewModel.kt | 12 ++--- .../InsulinManagementViewModel.kt | 5 +- .../app/aaps/ui/compose/main/MainViewModel.kt | 24 ++++----- .../compose/overview/OverviewDataCacheImpl.kt | 3 +- .../compose/overview/chips/ChipsViewModel.kt | 9 ++-- .../graphs/TreatmentBeltGraphCompose.kt | 4 +- .../profileHelper/ProfileHelperScreen.kt | 6 +-- .../profileManagement/ProfileEditorScreen.kt | 7 +-- .../viewmodels/ProfileManagementViewModel.kt | 3 +- .../quickLaunch/QuickLaunchResolver.kt | 2 +- .../compose/quickWizard/QuickWizardEditor.kt | 3 +- .../aaps/ui/compose/scenes/ActionEditors.kt | 3 +- .../ui/compose/scenes/wizard/DurationStep.kt | 3 +- .../ui/compose/scenesSheet/ScenesViewModel.kt | 3 +- .../aaps/ui/compose/stats/TddStatsCompose.kt | 5 +- .../tempBasalDialog/TempBasalDialogScreen.kt | 5 +- .../treatmentDialog/TreatmentDialogScreen.kt | 9 ++-- .../TreatmentDialogViewModel.kt | 12 ++--- .../ui/compose/treatments/BolusCarbsScreen.kt | 12 ++--- .../compose/treatments/ExtendedBolusScreen.kt | 2 +- .../ui/compose/treatments/TempBasalScreen.kt | 2 +- .../ui/compose/treatments/WizardInfoDialog.kt | 25 ++++----- .../viewmodels/BolusCarbsViewModel.kt | 5 +- .../treatmentsSheet/TreatmentViewModel.kt | 17 ++++--- .../wizardDialog/WizardDialogScreen.kt | 39 +++++++------- .../app/aaps/ui/search/BuiltInSearchables.kt | 2 +- .../ui/widget/glance/WidgetStateLoader.kt | 2 +- 138 files changed, 1110 insertions(+), 1088 deletions(-) diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt index f3a27792416c..0956b8e2a556 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSAMA/TestOpenAPSAMAPlugin.kt @@ -145,13 +145,13 @@ class TestOpenAPSAMAPlugin @Inject constructor( var minBg = hardLimits.verifyHardLimits( Round.roundTo(profile.getTargetLowMgdl(), 0.1), - app.aaps.core.ui.R.string.profile_low_target, + app.aaps.core.interfaces.R.string.profile_low_target, HardLimits.LIMIT_MIN_BG ) var maxBg = hardLimits.verifyHardLimits( Round.roundTo(profile.getTargetHighMgdl(), 0.1), - app.aaps.core.ui.R.string.profile_high_target, + app.aaps.core.interfaces.R.string.profile_high_target, HardLimits.LIMIT_MAX_BG ) var targetBg = @@ -179,14 +179,14 @@ class TestOpenAPSAMAPlugin @Inject constructor( HardLimits.LIMIT_TEMP_TARGET_BG ) } - if (!hardLimits.checkHardLimits(profile.iCfg.dia, app.aaps.core.ui.R.string.profile_dia, hardLimits.diaRange())) return + if (!hardLimits.checkHardLimits(profile.iCfg.dia, app.aaps.core.interfaces.R.string.profile_dia, hardLimits.diaRange())) return if (!hardLimits.checkHardLimits( profile.getIcTimeFromMidnight(MidnightUtils.secondsFromMidnight()), - app.aaps.core.ui.R.string.profile_carbs_ratio_value, + app.aaps.core.interfaces.R.string.profile_carbs_ratio_value, hardLimits.icRange() ) ) return - if (!hardLimits.checkHardLimits(profile.getIsfMgdl("test"), app.aaps.core.ui.R.string.profile_sensitivity_value, HardLimits.LIMIT_ISF)) return + if (!hardLimits.checkHardLimits(profile.getIsfMgdl("test"), app.aaps.core.interfaces.R.string.profile_sensitivity_value, HardLimits.LIMIT_ISF)) return if (!hardLimits.checkHardLimits(profile.getMaxDailyBasal(), app.aaps.core.ui.R.string.profile_max_daily_basal_value, 0.02, hardLimits.maxBasal())) return if (!hardLimits.checkHardLimits(ch.fromPump(pump.baseBasalRate), app.aaps.core.ui.R.string.current_basal_value, 0.01, hardLimits.maxBasal())) return startPart = System.currentTimeMillis() diff --git a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt index 5cb35da13080..29b12052f3a7 100644 --- a/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt +++ b/app/src/androidTest/kotlin/app/aaps/plugins/aps/openAPSSMB/TestOpenAPSSMBPlugin.kt @@ -148,13 +148,13 @@ open class TestOpenAPSSMBPlugin @Inject constructor( var minBg = hardLimits.verifyHardLimits( Round.roundTo(profile.getTargetLowMgdl(), 0.1), - app.aaps.core.ui.R.string.profile_low_target, + app.aaps.core.interfaces.R.string.profile_low_target, HardLimits.LIMIT_MIN_BG ) var maxBg = hardLimits.verifyHardLimits( Round.roundTo(profile.getTargetHighMgdl(), 0.1), - app.aaps.core.ui.R.string.profile_high_target, + app.aaps.core.interfaces.R.string.profile_high_target, HardLimits.LIMIT_MAX_BG ) var targetBg = @@ -182,14 +182,14 @@ open class TestOpenAPSSMBPlugin @Inject constructor( HardLimits.LIMIT_TEMP_TARGET_BG ) } - if (!hardLimits.checkHardLimits(profile.iCfg.dia, app.aaps.core.ui.R.string.profile_dia, hardLimits.diaRange())) return + if (!hardLimits.checkHardLimits(profile.iCfg.dia, app.aaps.core.interfaces.R.string.profile_dia, hardLimits.diaRange())) return if (!hardLimits.checkHardLimits( profile.getIcTimeFromMidnight(MidnightUtils.secondsFromMidnight()), - app.aaps.core.ui.R.string.profile_carbs_ratio_value, + app.aaps.core.interfaces.R.string.profile_carbs_ratio_value, hardLimits.icRange() ) ) return - if (!hardLimits.checkHardLimits(profile.getIsfMgdl("test"), app.aaps.core.ui.R.string.profile_sensitivity_value, HardLimits.LIMIT_ISF)) return + if (!hardLimits.checkHardLimits(profile.getIsfMgdl("test"), app.aaps.core.interfaces.R.string.profile_sensitivity_value, HardLimits.LIMIT_ISF)) return if (!hardLimits.checkHardLimits(profile.getMaxDailyBasal(), app.aaps.core.ui.R.string.profile_max_daily_basal_value, 0.02, hardLimits.maxBasal())) return if (!hardLimits.checkHardLimits(ch.fromPump(pump.baseBasalRate), app.aaps.core.ui.R.string.current_basal_value, 0.01, hardLimits.maxBasal())) return startPart = System.currentTimeMillis() diff --git a/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileCompareRowBuilders.kt b/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileCompareRowBuilders.kt index 9a162409e71c..8ad84c0f3491 100644 --- a/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileCompareRowBuilders.kt +++ b/core/graph/src/main/kotlin/app/aaps/core/graph/profile/ProfileCompareRowBuilders.kt @@ -8,6 +8,7 @@ import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.ui.R +import app.aaps.core.interfaces.R as InterfacesR /** * Pre-computed data for profile comparison (base vs effective, or any two profiles). @@ -54,9 +55,9 @@ fun buildProfileCompareData( baseName = profileName1, effectiveName = profileName2, shortHourUnit = rh.gs(app.aaps.core.interfaces.R.string.shorthour), - icUnits = rh.gs(R.string.profile_carbs_per_unit), - isfUnits = rh.gs(if (units == GlucoseUnit.MGDL) R.string.profile_isf_units_mgdl else R.string.profile_isf_units_mmol), - basalUnits = rh.gs(R.string.profile_ins_units_per_hour), + icUnits = rh.gs(InterfacesR.string.profile_carbs_per_unit), + isfUnits = rh.gs(if (units == GlucoseUnit.MGDL) InterfacesR.string.profile_isf_units_mgdl else InterfacesR.string.profile_isf_units_mmol), + basalUnits = rh.gs(InterfacesR.string.profile_ins_units_per_hour), targetUnits = units.displayLabel ) } diff --git a/core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml b/core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml index 23002596d8ef..d2f82dc6426d 100644 --- a/core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml +++ b/core/interfaces/src/androidMain/res/values-bg-rBG/strings.xml @@ -115,4 +115,37 @@ U200 U300 U500 + мг/дл/E + ммол/л/E + %1$+.2fЕ + %1$d гр + %1$s: %2$s + въглехидрати + Аларма след %1$d мин + Помпата е спряна + Loop изключен + Няма връзка с помпата + Базалните стойности не са за кръгли часове: %1$s + Базалната стойност е заместена от минимално поддържаната стойност: %1$s + Базалната стойност е заместена от максимално поддържаната стойност %1$s + Е/ч + гр/Е + %1$d мин + Запиши + Болус + УДЪЛЖЕН БОЛУС + Профил ниска цел + Профил висока цел + Профил време на действие на инсулин + Профил чувствителност + Профил въглехидратно число + »%1$s« %2$.2f е извън ограниченията + Базална стойност + COB срещу IOB + !!!!! Засечено бавно усвояване на въглехидрати: %1$d%% от времето. Проверете калкулацията. Въглехидратите може би са прекалено много и това ще доведе до много инсулин !!!!! + Приложено болус ограничение: %1$.2f Е към %2$.2f Е + Само запиши (без да да го стартираш в помпата) + Аларма, когато е време за хранене + Уд въглехидрати %1$dгр / %2$dч (+%3$dмин) + %1$.2fЕ diff --git a/core/interfaces/src/androidMain/res/values-ca-rES/strings.xml b/core/interfaces/src/androidMain/res/values-ca-rES/strings.xml index 33d27d8d3a0d..463d1c637cd6 100644 --- a/core/interfaces/src/androidMain/res/values-ca-rES/strings.xml +++ b/core/interfaces/src/androidMain/res/values-ca-rES/strings.xml @@ -40,4 +40,27 @@ + %1$+.2f U + %1$d g + Carbs + Executar alarma en %1$d min + Bomba aturada + Llaç aturat + Valors basals no alineats amb les hores: %1$s + Valor basal reemplaçat pel màxim valor acceptat: %1$s + U/h + g/U + %1$d min + Registre + Bolus + CARBS ESTESOS + Objectiu baix del perfil + Objectiu alt del perfil + Valor DIA del perfil + Valor sensibilitat del perfil + Valor ràtio de carbohidrats del perfil + »%1$s« %2$.2f està fora dels límits estrictes + Valor basal + COB vs IOB + Executar alarma quan sigui hora de menjar diff --git a/core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml b/core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml index 4a037251eb27..68b109c51902 100644 --- a/core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml +++ b/core/interfaces/src/androidMain/res/values-cs-rCZ/strings.xml @@ -121,4 +121,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Sacharidy + Spustit alarm za %1$d min + Pumpa pozastavena + Smyčka pozastavena + Pumpa odpojena + Hodnoty bazálů nejsou zarovnané na celé hodiny: %1$s + Hodnota bazálu byla nahrazena minimální možnou: %1$s + Hodnota bazálu nahrazena maximální možnou: %1$s + U/h + g/U + %1$d min + Záznam + Bolus + ROZLOŽENÉ SACHARIDY + Dolní cíl profilu + Horní cíl profilu + Hodnota DIA profilu + Hodnota citlivosti profilu + Inzulino-sacharidový poměr profilu + »%1$s« %2$.2f je mimo pevně nastavené limity + Hodnota bazálu + COB vs. IOB + !!!!! Byla zjištěna pomalá absorpce sacharidů: %1$d%% času. Zkontrolujte svůj výpočet. COB může být nadhodnoceno, takže by mohlo být podáno více inzulínu !!!!! + Použito omezení bolusu: %1$.2f U na %2$.2f U + Bolus nebude pumpou vydán, pouze zaznamenán + Spustit alarm, když je čas na jídlo + eSach %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-da-rDK/strings.xml b/core/interfaces/src/androidMain/res/values-da-rDK/strings.xml index 02ae26204847..145542501fc4 100644 --- a/core/interfaces/src/androidMain/res/values-da-rDK/strings.xml +++ b/core/interfaces/src/androidMain/res/values-da-rDK/strings.xml @@ -63,4 +63,31 @@ + %1$+.2f IE + %1$d g + Kulhydrater + Kør alarm om %1$d min + Pumpe er afbrudt + Loop suspenderet + Basalværdier ikke angivet i hele timer: %1$s + Basal dosis erstattet af minimal understøttet dosis: %1$s + Basal dosis erstattet af maximal understøttet dosis: %1$s + E/t + g/E + %1$d min + Registrér + Bolus + FORLÆNGET KH + Nedre målværdi for profilen + Øvre målværdi for profilen + Profil DIA værdi + Profil følsomhed værdi + Profil KH ratio værdi + »%1$s« %2$.2f er uden for absolutte grænser + Basalværdi + COB vs IOB + Bolus-begrænsning anvendt: %1$.2f IE til %2$.2f IE + Bolus registreres kun (bliver ikke leveret af pumpe) + Kør alarm når det er tid til at spise + %1$.2f IE diff --git a/core/interfaces/src/androidMain/res/values-de-rDE/strings.xml b/core/interfaces/src/androidMain/res/values-de-rDE/strings.xml index 53a3c1423bc1..bc8f7b462721 100644 --- a/core/interfaces/src/androidMain/res/values-de-rDE/strings.xml +++ b/core/interfaces/src/androidMain/res/values-de-rDE/strings.xml @@ -64,4 +64,31 @@ + %1$+.2f IE + %1$d g + Kohlenhydrate + Alarm in %1$d Min. + Pumpe pausiert + Loop pausiert + Basalraten beginnen nicht zur vollen Stunde: %1$s + Basal-Wert wurde durch den kleinst möglichen Wert ersetzt: %1$s + Basal-Wert wurde durch größt möglichen Wert ersetzt: %1$s + IE/h + g/IE + %1$d min. + Eintrag + Bolus + VERLÄNGERTE KOHLENHYDRATE + Profil unteres Ziel + Profil oberes Ziel + Profil Insulinwirkdauer + Profil Sensitivitätswert + Profil KH-Faktor + »%1$s« %2$.2f ist außerhalb der fest programmierten Grenzen + Basal-Wert + COB vs IOB + Bolus Einschränkung angewendet: %1$.2f U bis %2$.2f U + Bolus wird nur aufgezeichnet (Die Pumpe gibt kein Insulin ab!) + Alarmiere mich, wenn es Zeit zum Essen ist. + %1$.2f IE diff --git a/core/interfaces/src/androidMain/res/values-el-rGR/strings.xml b/core/interfaces/src/androidMain/res/values-el-rGR/strings.xml index 5c65459a6287..fac2c7314a0d 100644 --- a/core/interfaces/src/androidMain/res/values-el-rGR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-el-rGR/strings.xml @@ -63,4 +63,31 @@ + %1$+.2f U + %1$d g + Υδατάνθρακες + Εκτέλεση συναγερμού σε %1$d λεπτά + Η αντλία είναι σε παύση + Κύκλωμα σε αναστολή + Οι τιμές του βασικού ρυθμού δεν αντιστοιχούν σε ώρες: %1$s + Η τιμή του βασικού αντικαταστάθηκε από την ελάχιστη υποστηριζόμενη τιμή: %1$s + Η τιμή του βασικού αντικαταστάθηκε από την μέγιστη υποστηριζόμενη τιμή: %1$s + U/h + g/U + %1$d λεπτά + Εγγραφή + Bolus + ΕΚΤΕΤΑΜΕΝΟΙ ΥΔΑΤΑΝΘΡΑΚΕΣ + Χαμηλός στόχος προφίλ + Υψηλός στόχος προφίλ + Τιμή DIA του προφίλ + Τιμή ευαισθησίας προφίλ + Τιμή αναλογίας υδατανθράκων προφίλ + Η τιμή »%1$s« %2$.2f είναι εκτός ορίων + Τιμή Βασικού ρυθμού + COB vs IOB (ενεργοί υδατάνθρακες vs ενεργή ινσουλίνη) + Ορίστηκε περιορισμός Bolus: %1$.2f U σε %2$.2f U + Το Bolus μόνο θα καταγραφεί (δε θα χορηγηθεί από την αντλία) + Εκτέλεση συναγερμού όταν έρθει η ώρα να φάτε + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-es-rES/strings.xml b/core/interfaces/src/androidMain/res/values-es-rES/strings.xml index 017a7fadb5c7..72f6350d354b 100644 --- a/core/interfaces/src/androidMain/res/values-es-rES/strings.xml +++ b/core/interfaces/src/androidMain/res/values-es-rES/strings.xml @@ -115,4 +115,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Carbohidratos + Ejecutar alarma en %1$d min + Bomba suspendida + Lazo suspendido + Bomba desconectada + Valores basales no alineados a las horas: %1$s + Valor basal cambiado al valor mínimo soportado: %1$s + Valor basal reemplazado por el valor máximo soportado: %1$s + U/h + g/U + %1$d min + Registro + Bolo + CARBOHIDRATOS EXTENDIDOS + Perfil de objetivo bajo + Perfil de objetivo alto + Valor DIA del perfil + Valor de sensibilidad del perfil + Valor del ratio de carbohidratos del perfil + »%1$s« %2$.2f está fuera de los límites estrictos + Valor basal + COB vs IOB + ¡¡¡Absorción lenta de hidratos detectada: %1$d%% del tiempo. Revisa tus cálculos. Los COB pueden estar sobreestimados y el sistema podría darte demasiada insulina!!! + Restricción de bolo aplicada: %1$.2f U a %2$.2f U + El bolo sólo se anotará (no será entregado por la bomba) + Ejecutar alarma cuando sea hora de comer + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml b/core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml index f2383e366834..1d163b670b01 100644 --- a/core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-fr-rFR/strings.xml @@ -115,4 +115,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Glucides + Alerter dans %1$d min + Pompe arrêtée + La Boucle est suspendue + Pompe déconnectée + Valeurs des débits de basal non alignées sur des heures: %1$s + Valeur de basal remplacée par la valeur minimale autorisée : %1$s + Valeur de basal remplacée par la valeur maximale autorisée : %1$s + U/h + g/U + %1$d min + Enregistrement + Bolus + GLUCIDES ÉTENDUS + Cible basse du profil + Cible haute du profil + Valeur DAI du profil + Valeur de sensibilité du profil + Rapport glucides/insuline de profil + \"%1$s\" %2$.2f est en dehors des limites + Valeur de Basal + GA vs IA + !!!!! Absorption lente des glucides détectée dans %1$d%% des cas. Vérifiez de nouveau votre calcul. Les GA (Glucides Actifs) peuvent être surestimés et alors trop d\'insuline pourrait être injectée !!!!! + Contrainte de Bolus appliquée : %1$.2f U vers %2$.2f U + Les bolus seront seulement enregistrés (pas délivrés par la pompe) + Alerter quand il est temps de manger + eGlucides %1$d g / %2$d h (+%3$d min) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml b/core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml index 6bb9ce88c443..668813f4fa21 100644 --- a/core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-hr-rHR/strings.xml @@ -31,4 +31,20 @@ + %1$+.2f U + %1$d g + UH + Uključi alarm za %1$d min + Petlja suspendirana + Bazalne vrijednosti nisu usklađene sa satima: %1$s + Bazalna vrijednost zamijenjena minimalnom podržanom vrijednošću: %1$s + Bazalna vrijednost zamijenjena maksimalnom podržanom vrijednošću: %1$s + U/h + g/U + Bolus + Profil niski cilj + Profil visoki cilj + Profil DIA vrijednost + Vrijednost osjetljivosti profila + Vrijednost omjera ugljikohidrata profila diff --git a/core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml b/core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml index 3b0b5e398458..30a17d0b11fc 100644 --- a/core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml +++ b/core/interfaces/src/androidMain/res/values-hu-rHU/strings.xml @@ -57,4 +57,9 @@ + %1$+.2f E + E/ó + g/E + Bólus + %1$.2f E diff --git a/core/interfaces/src/androidMain/res/values-it-rIT/strings.xml b/core/interfaces/src/androidMain/res/values-it-rIT/strings.xml index b76ab36086c4..db6a32aa013e 100644 --- a/core/interfaces/src/androidMain/res/values-it-rIT/strings.xml +++ b/core/interfaces/src/androidMain/res/values-it-rIT/strings.xml @@ -115,4 +115,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + CHO + Esegui allarme in %1$d min + Micro sospeso + Loop sospeso + Micro disconnesso + Valori basali non allineati alle ore: %1$s + Valore basale sostituito dal minimo valore supportato: %1$s + Valore basale sostituito dal massimo valore supportato: %1$s + U/h + g/U + %1$d min + Record + Bolo + CHO ESTESI + Target basso (profilo) + Target alto (profilo) + Valore DIA (profilo) + Valore sensibilità (profilo) + Valore rapporto CHO (profilo) + »%1$s« %2$.2f è fuori dai limiti consentiti + Valore basale + COB vs IOB + !!!!! Rilevato assorbimento lento dei carboidrati: %1$d%% del tempo. Ricontrolla il tuo calcolo. I COB potrebbero essere sovrastimati e potrebbe essere somministrata più insulina !!!!! + Vincolo bolo applicato: %1$.2f U a %2$.2f U + Il bolo sarà solo registrato (non erogato dal micro) + Esegui allarme quando è tempo di mangiare + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml b/core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml index f09e639c8e44..31844860a3ef 100644 --- a/core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml +++ b/core/interfaces/src/androidMain/res/values-iw-rIL/strings.xml @@ -69,4 +69,31 @@ + %1$+.2f יח\' + %1$d גר\' + פחמימות + הפעל התראה בעוד %1$d דקות + משאבה מושהית + לולאה מושהית + ערכי הבזאלי לא מותאמים לשעות: %1$s + ערכי הבזאלי הוחלפו בערכים הנתמכים המינימליים: %1$s + ערכי הבזאלי הוחלפו בערכים הנתמכים המינימליים: %1$s + יח\'\\שעה + גר\'\\יח\' + %1$d דק\' + הקלטה + בולוס + פחמימות מורכבות + ערך המטרה הנמוך של הפרופיל + ערך המטרה הגבוה של הפרופיל + ערך DIA של הפרופיל + ערך הרגישות של הפרופיל + יחס הפחמימות של הפרופיל + »הערך %1$s« %2$.2f מחוץ לתחום הקשיח + ערך בזאלי + פחמ\' פעילות לעומת אינ\' פעיל + מגבלת בולוס יושמה: %1$.2f עד %2$.2f יח\' + בולוס רשום בלבד (לא מוזרק על ידי המשאבה) + הפעל התראה כשצריכים לאכול + %1$.2f יח\' diff --git a/core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml b/core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml index bbc60da1f588..dee7009c383d 100644 --- a/core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-ko-rKR/strings.xml @@ -64,4 +64,31 @@ + %1$+.2f U + %1$d g + 탄수화물 + %1$d분 뒤 알람 울림 + 펌프 일시중지됨 + Loop 일시중지 + Basal값이 시간단위로 설정되지 않았습니다: %1$s + 지원되는 최대 값으로 Basal 값이 대체되었습니다: %1$s + 지원되는 최대값으로 Basal값이 대체되었습니다:%1$s + U/h + g/U + %1$d 분 + 기록 + Bolus + 확장 탄수화물 + 프로파일 저혈당 목표 + 프로파일 고혈당 목표 + 프로파일 DIA 값 + 프로파일 민감도 값 + 프로파일의 탄수화물 비율 값 + »%1$s« %2$.2f이 \'고정된 한계값\'을 벗어났습니다. + Basal 값 + COB vs IOB + 적용된 Bolus 제약 조건: %1$.2f U 에서 %2$.2f U + Bolus는 이 경우(펌프를 통해 공급되지 않음) 에만 기록됩니다 + 식사 시간이 되면 알람을 울리기 + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml b/core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml index cf5fc1a3c5c4..b8f5c8f44399 100644 --- a/core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml +++ b/core/interfaces/src/androidMain/res/values-lt-rLT/strings.xml @@ -72,4 +72,31 @@ + %1$+.2f vv + %1$dg + Angliavandeniai + Pranešti po %1$d min + Pompa sustabdyta + Ciklas sustabdytas + Bazės reikšmės nesuderintos su valandomis: %1$s + Nustatyta mažiausia galima valandinės bazės vertė: %1$s + Nustatyta didžiausia galima valandinės bazės vertė: %1$s + v/val + g/v + %1$d min. + Įrašas + Bolusas + IŠTĘSTI AV + Profilio tikslo žemoji riba + Profilio tikslo aukštoji riba + Profilio IVT reikšmė + Profilio JIF reikšmė + Profilio IA reikšmė + »%1$s« %2$.2f viršija griežtą limitą + Valandinė bazė + AAO prieš AIO + Pritaikytas boluso apribojimas: %1$.2f v iki %2$.2f v + Bolusas bus tik įrašytas (nebus suleistas) + Pranešti apie laiką valgyti + %1$.2f V diff --git a/core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml b/core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml index 1803f76ec68b..0996ba83b13e 100644 --- a/core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml +++ b/core/interfaces/src/androidMain/res/values-nb-rNO/strings.xml @@ -115,4 +115,37 @@ U200 U300 U500 + mg/dL/E + mmol/L/E + %1$+.2f E + %1$d g + %1$s: %2$s + Karbo + Aktiver alarm om %1$d min + Pumpen er pauset + Loop pauset + Pumpe frakoblet + Basalverdier er ikke angitt på hele timer: %1$s + Basalverdi erstattet med minste tillate verdi: %1$s + Basalverdi erstattet med høyeste tillate verdi: %1$s + E/t + g/E + %1$d min + Registrer + Bolus + FORLENGET KARBO + Profil lavt mål + Profil høyt mål + Profil DIA verdi + Profilens insulinfølsomhet + Profilens insulin-til-karbohydratforhold (IK) + »%1$s« %2$.2f er utenfor lovlige grenseverdier + Basalverdi + COB vs IOB + !!!!! Langsom karboabsorpsjon oppdaget: %1$d%% av tiden. Dobbeltsjekk beregningen din. COB kan overestimeres, og dermed kan mer insulin gis !!!!! + Bolus begrensning brukt: %1$.2f E til %2$.2f E + Bolus vil bare bli loggført (ikke levert av pumpe) + Aktiver alarm når det er på tide å spise + eKarbo %1$dg / %2$dt (+%3$dmin) + %1$.2f E diff --git a/core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml b/core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml index 75085630e9d5..90567de4bb28 100644 --- a/core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml +++ b/core/interfaces/src/androidMain/res/values-nl-rNL/strings.xml @@ -93,4 +93,32 @@ + %1$+.2f E + %1$d g + Koolhydraten + Start alarm over %1$d min + Pomp onderbreken + Loop pauzeren + Pomp niet verbonden + Basaalstanden niet ingesteld in hele uren: %1$s + Minimum basaalwaarde is vervangen door minimaal ondersteunde waarde: %1$s + Basale waarde vervangen door maximale ondersteunde waarde: %1$s + E/u + g/E + %1$d min + Opnemen + Bolus + VERLENGDE KOOLHYDRATEN + Profiel laag doel + Profiel hoog doel + Profiel DIA waarde + Profiel gevoeligheidswaarde + Profiel koolhydraten ratio waarde + »%1$s« %2$.2f is buiten de harde limiet + Basaal waarde + COB vs IOB + Bolusbeperking toegepast: %1$.2f E naar %2$.2f E + Bolus wordt alleen geregistreerd (niet toegediend door pomp) + Start alarm wanneer het tijd is om te eten + %1$.2f E diff --git a/core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml b/core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml index 635431130f1b..b1d76a4998e3 100644 --- a/core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml +++ b/core/interfaces/src/androidMain/res/values-pl-rPL/strings.xml @@ -98,4 +98,32 @@ + %1$+.2f U + %1$d g + Węglowodany + Uruchom alarm za %1$d min + Pompa wstrzymana + Pętla wstrzymana + Pompa odłączona + Wartości bazy nie są ustawione w pełnych godzinach: %1$s + Wartość bazy zastąpiona minimalną obsługiwaną wartością: %1$s + Wartość bazy zastąpiona maksymalną obsługiwaną wartością: %1$s + U/h + g/U + %1$d min + Wpis + Bolus + PRZEDŁUŻONE WĘGLOWODANY + Dolna granica celu profilu + Górna granica celu profilu + Wartość DIA profilu + Wartość wrażliwości profilu + Stosunek węglowodanów profilu + Wartość »%1$s« %2$.2f jest poza dopuszczalną granicą + Wartość bazy + COB vs IOB + Zastosowano ograniczenie bolusa: %1$.2f U do %2$.2f U + Bolus zostanie jedynie odnotowany (nie będzie podany przez pompę) + Uruchom alarm kiedy będzie czas na jedzenie + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml b/core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml index b0c40c560016..6c044f1799ec 100644 --- a/core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-pt-rBR/strings.xml @@ -61,4 +61,30 @@ + %1$+.2f U + %1$d g + Carbos + Disparar alarme em %1$d min + Bomba suspensa + Loop suspenso + Valores das basais não definidos por horas: %1$s + Valor da basal alterado para o valor mínimo suportado: %1$s + Valor da basal alterado para o valor máximo suportado: %1$s + U/h + g/U + %1$d min + Gravar + Bólus + CARBOIDRATOS ESTENDIDOS + Alvo de perfil de hipoglicemia + Alvo de perfil de hiperglicemia + Valor do perfil da DAI + Valor do perfil de sensibilidade + Valor do perfil da taxa de carboidratos + »%1$s« %2$.2f está fora dos limites estabelecidos + Valor basal + CA vs IA + Restrição de bólus aplicada: %1$.2f U para %2$.2f U + Bolus será apenas registrado (não administrado pela bomba) + Disparar alarme quando for a hora de comer diff --git a/core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml b/core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml index bb509112d76b..c529b46ff64e 100644 --- a/core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml +++ b/core/interfaces/src/androidMain/res/values-pt-rPT/strings.xml @@ -61,4 +61,29 @@ + %1$+.2f U + %1$d g + Hidratos + Executar alarme em %1$d min + Bomba suspensa + Loop suspenso + Valores das basais não definidos por horas: %1$s + Valor da basal alterado para o valor mínimo suportado: %1$s + Valor da basal alterado para o valor máximo suportado: %1$s + U/h + g/U + %1$d min + Registo + Bólus + HC LENTOS + Valor mínimo alvo do perfil + Valor máximo alvo do perfil + Valor Perfil DIA + Valor Perfil Sensibilidade + Valor Perfil Rácio Hidratos + »%1$s« %2$.2f está fora dos limites permitidos + Valor da Basal + HCA vs IA + Executar alarme quando for tempo de comer + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml b/core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml index 85de88413b04..fe0a34f28dac 100644 --- a/core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml +++ b/core/interfaces/src/androidMain/res/values-ro-rRO/strings.xml @@ -118,4 +118,37 @@ U200 U300 U500 + mg/dL/U + mmol/l/U + %1$+.2f U + %1$d g + %1$s: %2$s + Carbohidrați + Rulați alarma în %1$d minute + Pompă suspendată + Buclă suspendată + Pompă deconectată + Valori bazale nesincronizate cu ora: %1$s + Valoarea bazalei a fost înlocuită cu valoarea minimă posibilă: %1$s + Valoarea bazalei a fost înlocuită cu valoarea maximă posibilă: %1$s + U/h + g/U + %1$d min + Înregistrare + Bolus + CARBOHIDRAȚI EXTINȘI + Profil ținta joasă + Profil țintă ridicată + Valoare profil DIA + Valoare sensibilitate profil + Valoarea raportului carbohidrați din profil + »%1$s« %2$.2f este in afara limitelor stabilite + Valoare rata bazală + COB vs IOB + !!!!! S-a detectat o absorbție lentă de carbohidrați: %1$d%% din timp. Verificați de două ori calculul. COB poate fi supraestimat, astfel încât mai multă insulină poate fi administrată !!!!! + Este aplicată limitarea bolusului %1$.2f U la %2$.2f U + Bolusul doar va fi înregistrat (nu va fi administrat de pompă) + Executați alarma când este timpul să mâncați + carbohidrați extinși %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml b/core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml index 0c1dad398fb8..0855704ee0a4 100644 --- a/core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml +++ b/core/interfaces/src/androidMain/res/values-ru-rRU/strings.xml @@ -121,4 +121,32 @@ 200 ед 300 ед 500 ед + %1$+.2f ед + %1$d гр + Углеводы + Напомнить через %1$d мин + Работа помпы остановлена + ЗЦ остановлен + Помпа отключена + Базальные значения не выровнены по часам: %1$s + Значение базала заменено минимальной поддерживаемой величиной: %1$s + Значение базала заменено максимальной поддерживаемой величиной: %1$s + ед/ч + г/ед + %1$d мин. + Запись + Болюс + ПРОЛОНГИРОВАННЫЕ УГЛЕВОДЫ + Нижнее целевое значение профиля + Верхнее целевое значение профиля + Значение длительности действия инсулина DIA в профиле + Значение чувствительности в профиле + Значение Углеводного коэффициента IC в профиле + »%1$s« %2$.2f за пределами жестких ограничений + Величина базала + угл COB к инс IOB + Применено ограничение болюса: %1$.2f ед. до %2$.2f ед. + Болюс будет только записан (без подачи помпой) + Напомнить о еде + %1$.2f ед diff --git a/core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml b/core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml index f567a8da7497..9d1caaf4296b 100644 --- a/core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml +++ b/core/interfaces/src/androidMain/res/values-sk-rSK/strings.xml @@ -121,4 +121,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Sacharidy + Spustiť výstrahu za %1$d min + Pumpa pozastavená + Uzavretý okruh pozastavený + Pumpa odpojená + Bazálne hodnoty nie sú zarovnané na celé hodiny: %1$s + Hodnota bazálu nahradená minimálnou možnou: %1$s + Hodnota bazálu nahradená maximálnou možnou: %1$s + U/h + g/U + %1$d min. + Záznam + Bolus + ROZLOŽENÉ SACHARIDY + Dolný cieľ profilu + Horný cieľ profilu + Profilová hodnota DIA + Profilová hodnota citlivosti + Profilový inzulino-sacharidový pomer + »%1$s« %2$.2f je mimo pevne nastavených limitov + Hodnota bazálu + COB vs. IOB + !!!!! Detekovaná pomalá absorbcia sacharidov: %1$d%% času. Radšej dvakrát skontrolujte kalkuláciu. COB môže byť úplne iné, môže byť podaného viac inzulínu!!!!! + Použité obmedzenie bolusu: %1$.2f U na %2$.2f U + Bolus bude iba zaznamenaný (nie pumpou vydaný) + Spustiť výstrahu, keď je čas na jedlo + eSacharidy %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml b/core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml index af6285f56cdd..a3fe5dbba241 100644 --- a/core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml +++ b/core/interfaces/src/androidMain/res/values-sr-rCS/strings.xml @@ -23,4 +23,6 @@ + Uglj. hidrati + Bolus diff --git a/core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml b/core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml index 849aa94dfdfb..7d26e3f953cc 100644 --- a/core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml +++ b/core/interfaces/src/androidMain/res/values-sv-rSE/strings.xml @@ -63,4 +63,31 @@ + %1$+.2f U + %1$dg + Kolhydrater + Larma om %1$d min + Pump pausad + Loop pausad + Profilens basaler är inte satta på hel timme: %1$s + Basalvärdet ersatt med det lägsta tillåtna: %1$s + Basalvärdet ersatt med det högsta tillåtna: %1$s + U/h + g/U + %1$d min + Post + Bolus + FÖRLÄNGDA KH + Nedre målvärde för profilen + Övre målvärde för profilen + Profilens DIA + Profilens insulinkänslighetsvärde + Profilens KH-kvot + »%1$s« %2$.2f är utanför hårda gränser + Basaldos + COB kontra IOB + Bolusbegränsning tillämpad: %1$.2f U till %2$.2f U + Bolus kommer bara att loggas (inte levereras av pumpen) + Larma när det är dags att äta + %1$.2fU diff --git a/core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml b/core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml index d05ac9c9779e..0d11ed08e167 100644 --- a/core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml +++ b/core/interfaces/src/androidMain/res/values-tr-rTR/strings.xml @@ -72,4 +72,32 @@ + %1$+.2f Ü + %1$d g + Karbonhidrat + Alarmı %1$d dakika içinde çalıştır + Pompa Durduruldu + Döngü duraklatıldı + Pompa bağlantısı kesildi + Bazal değerler saatlerle uyumlu değil: %1$s + Desteklenen minimum değerle değiştirilen bazal değer: %1$s + Bazal değeri maksimum desteklenen değerle değiştirilir: %1$s + Ü/s + g/Ü + %1$d dak + Kayıt + Bolus + YAYMA KARBONHİDRAT + Düşük hedef profili + Yüksek hedef profili + Profil DIA değeri + Profil duyarlılık değeri + Profil karbonhidrat oranı değeri + »%1$s« %2$.2f sınırların dışında + Bazal değer + AKRB vs AİNS + Bolus kısıtlaması uygulandı: %1$.2f Ü ile %2$.2f Ü + Bolus yalnızca kaydedilecektir (pompa ile iletilmez) + Yemek zamanı alarmı çalıştır + %1$.2f Ü diff --git a/core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml b/core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml index bba313568cff..69e8a885da15 100644 --- a/core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml +++ b/core/interfaces/src/androidMain/res/values-uk-rUA/strings.xml @@ -60,4 +60,5 @@ + %1$.2f од diff --git a/core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml b/core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml index 58de1e23a3d5..0c695c39d5ee 100644 --- a/core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml +++ b/core/interfaces/src/androidMain/res/values-vi-rVN/strings.xml @@ -112,4 +112,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Carbs + Báo động sau %1$d phút + Bơm đã tạm dừng + Vòng lặp đã tạm dừng + Bơm đã ngắt kết nối + Giá trị liều nền không khớp với giờ: %1$s + Giá trị liều nền đã được thay bằng giá trị tối thiểu được hỗ trợ: %1$s + Giá trị liều nền đã được thay bằng giá trị tối đa được hỗ trợ: %1$s + U/h + g/U + %1$d phút + Record + Bolus + CARBS MỞ RỘNG + Cấu hình mục tiêu thấp + Cấu hình mục tiêu cao + Cấu hình giá trị DIA + Cấu hình giá trị độ nhạy + Cấu hình tỷ lệ Carb + »%1$s« %2$.2f nằm ngoài giới hạn an toàn + Giá trị liều liều nền + COB với IOB + !!!!! Phát hiện hấp thu carb chậm trong %1$d%% thời gian. Hãy kiểm tra lại phép tính của bạn. COB có thể bị ước tính cao, do đó có thể dẫn đến việc cung cấp nhiều insulin hơn !!!!! + Giới hạn bolus đã áp dụng: %1$.2f U đến %2$.2f U + Liều Bolus chỉ được ghi lại (không tiêm qua Bơm) + Chạy báo thức khi đến giờ ăn + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml b/core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml index b00f2290749b..5310cddf1193 100644 --- a/core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml +++ b/core/interfaces/src/androidMain/res/values-zh-rCN/strings.xml @@ -112,4 +112,37 @@ U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d 克 + %1$s: %2$s + 碳水化合物 + 在 %1$d 分钟内运行提醒 + 泵暂停了 + 闭环暂停了 + 泵已断开 + 基础率值与小时数不一致:%1$s + 基础率已设为支持的最小值:%1$s + 基础率值被泵支持的最大值:%1$s 替换了 + U/h + 克/U + %1$d 分钟 + 记录 + 大剂量 + 扩展碳水 + 个人配置低目标 + 个人配置高目标 + 配置文件DIA值 + 配置文件敏感系数值 + 配置文件碳水系数值 + »%1$s« %2$.2f 超出了硬限制 + 基础率值 + 活性碳水vs活性胰岛素 + !!!!!检测到碳水化合物吸收缓慢:%1$d%%的时间。仔细检查你的计算。COB可能被高估,因此可能注射过多的胰岛素!!!!! + 已应用推注限制: %1$.2f U 到 %2$.2f U + 仅记录大剂量数值(泵不会输注) + 在应当吃饭时提醒 + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml b/core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml index 83891176af55..05c7da8dccff 100644 --- a/core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml +++ b/core/interfaces/src/androidMain/res/values-zh-rTW/strings.xml @@ -124,4 +124,37 @@ Other U200 U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + 碳水化合物 + 在 %1$d 分鐘內運行警報 + 幫浦已暫停 + 循環已暫停 + 幫浦已中斷連線 + 基礎率值未對齊小時:%1$s + 基礎率值已被最低支援值取代:%1$s + 基礎率值已被最高支援值取代:%1$s + U/h + g/U + %1$d 分鐘 + 紀錄 + 注射 + 延長碳水化合物 + 設定檔低目標 + 設定檔高目標 + 設定檔 DIA 值 + 設定檔敏感度值 + 設定檔碳水化合物比率值 + »%1$s« %2$.2f 超出硬限制範圍 + 基礎率值 + COB 對 IOB + !!!!! 偵測到碳水吸收偏慢:%1$d%% 的時間。請再次檢查你的計算。活性碳水化合物可能被高估,因此可能會注射過多胰島素 !!!!! + 注射限制已套用:%1$.2f U 至 %2$.2f U + 僅紀錄注射(不由幫浦傳送) + 當到達用餐時間時提醒我 + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/interfaces/src/androidMain/res/values/strings.xml b/core/interfaces/src/androidMain/res/values/strings.xml index f307b7a52071..98849418971f 100644 --- a/core/interfaces/src/androidMain/res/values/strings.xml +++ b/core/interfaces/src/androidMain/res/values/strings.xml @@ -125,4 +125,37 @@ U300 U500 + mg/dL/U + mmol/L/U + %1$+.2f U + %1$d g + %1$s: %2$s + Carbs + Run alarm in %1$d min + Pump suspended + Loop suspended + Pump disconnected + Basal values not aligned to hours: %1$s + Basal value replaced by minimum supported value: %1$s + Basal value replaced by maximum supported value: %1$s + U/h + g/U + %1$d min + Record + Bolus + EXTENDED CARBS + Profile low target + Profile high target + Profile DIA value + Profile sensitivity value + Profile carbs ratio value + »%1$s« %2$.2f is out of hard limits + Basal value + COB vs IOB + !!!!! Slow carbs absorption detected: %1$d%% of time. Double check your calculation. COB can be overestimated thus more insulin could be given !!!!! + Bolus constraint applied: %1$.2f U to %2$.2f U + Bolus will be recorded only (not delivered by pump) + Run alarm when is time to eat + eCarbs %1$dg / %2$dh (+%3$dmin) + %1$.2f U diff --git a/core/objects/build.gradle.kts b/core/objects/build.gradle.kts index 3300912e32e7..86d683b85878 100644 --- a/core/objects/build.gradle.kts +++ b/core/objects/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(project(":core:data")) implementation(project(":core:interfaces")) implementation(project(":core:keys")) - implementation(project(":core:ui")) implementation(project(":core:utils")) testImplementation(project(":shared:tests")) diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt index 6ec06f284dea..8a9ceec39f72 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/profile/ProfileSealed.kt @@ -32,7 +32,7 @@ import app.aaps.core.objects.extensions.shiftBlock import app.aaps.core.objects.extensions.shiftTargetBlock import app.aaps.core.objects.extensions.targetBlockValueBySeconds import app.aaps.core.objects.extensions.toJsonObject -import app.aaps.core.ui.R +import app.aaps.core.interfaces.R import app.aaps.core.utils.MidnightUtils import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/runningMode/RunningModeGuard.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/runningMode/RunningModeGuard.kt index d5c45dafacd1..267b74c31447 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/runningMode/RunningModeGuard.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/runningMode/RunningModeGuard.kt @@ -4,7 +4,7 @@ import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventShowSnackbar -import app.aaps.core.ui.R +import app.aaps.core.interfaces.R import javax.inject.Inject import javax.inject.Singleton diff --git a/core/objects/src/main/kotlin/app/aaps/core/objects/wizard/BolusWizard.kt b/core/objects/src/main/kotlin/app/aaps/core/objects/wizard/BolusWizard.kt index dd85a53520c1..91f9d3f93400 100644 --- a/core/objects/src/main/kotlin/app/aaps/core/objects/wizard/BolusWizard.kt +++ b/core/objects/src/main/kotlin/app/aaps/core/objects/wizard/BolusWizard.kt @@ -382,24 +382,24 @@ class BolusWizard @Inject constructor( line( ConfirmationRole.BOLUS, rh.gs( - app.aaps.core.ui.R.string.confirmation_line, - rh.gs(app.aaps.core.ui.R.string.bolus), - rh.gs(app.aaps.core.ui.R.string.format_insulin_units, insulinAfterConstraints) + pct + app.aaps.core.interfaces.R.string.confirmation_line, + rh.gs(app.aaps.core.interfaces.R.string.bolus), + rh.gs(app.aaps.core.interfaces.R.string.format_insulin_units, insulinAfterConstraints) + pct ) ) } if (carbs > 0 && !advisor) { val timeShift = when { - carbTime > 0 -> " (+" + rh.gs(app.aaps.core.ui.R.string.mins, carbTime) + ")" - carbTime < 0 -> " (" + rh.gs(app.aaps.core.ui.R.string.mins, carbTime) + ")" + carbTime > 0 -> " (+" + rh.gs(app.aaps.core.interfaces.R.string.mins, carbTime) + ")" + carbTime < 0 -> " (" + rh.gs(app.aaps.core.interfaces.R.string.mins, carbTime) + ")" else -> "" } line( ConfirmationRole.CARBS, rh.gs( - app.aaps.core.ui.R.string.confirmation_line, - rh.gs(app.aaps.core.ui.R.string.carbs), - rh.gs(app.aaps.core.ui.R.string.format_carbs, carbs) + timeShift + app.aaps.core.interfaces.R.string.confirmation_line, + rh.gs(app.aaps.core.interfaces.R.string.carbs), + rh.gs(app.aaps.core.interfaces.R.string.format_carbs, carbs) + timeShift ) ) } @@ -407,30 +407,30 @@ class BolusWizard @Inject constructor( line( ConfirmationRole.COB, rh.gs( - app.aaps.core.ui.R.string.confirmation_line, - rh.gs(app.aaps.core.ui.R.string.cobvsiob), + app.aaps.core.interfaces.R.string.confirmation_line, + rh.gs(app.aaps.core.interfaces.R.string.cobvsiob), rh.gs( - app.aaps.core.ui.R.string.formatsignedinsulinunits, + app.aaps.core.interfaces.R.string.formatsignedinsulinunits, -insulinFromBolusIOB - insulinFromBasalIOB + insulinFromCOB + insulinFromBG ) ) ) val absorptionRate = iobCobCalculator.ads.slowAbsorptionPercentage(60) if (absorptionRate > .25) { - line(ConfirmationRole.COB, rh.gs(app.aaps.core.ui.R.string.slowabsorptiondetected_plain, (absorptionRate * 100).toInt())) + line(ConfirmationRole.COB, rh.gs(app.aaps.core.interfaces.R.string.slowabsorptiondetected_plain, (absorptionRate * 100).toInt())) } } if (abs(insulinAfterConstraints - calculatedTotalInsulin) > ch.bolusStep(insulinAfterConstraints)) { - line(ConfirmationRole.WARNING, rh.gs(app.aaps.core.ui.R.string.bolus_constraint_applied_warn, calculatedTotalInsulin, insulinAfterConstraints)) + line(ConfirmationRole.WARNING, rh.gs(app.aaps.core.interfaces.R.string.bolus_constraint_applied_warn, calculatedTotalInsulin, insulinAfterConstraints)) } if ((config.AAPSCLIENT || forcedRecordOnly) && insulinAfterConstraints > 0) { - line(ConfirmationRole.WARNING, rh.gs(app.aaps.core.ui.R.string.bolus_recorded_only)) + line(ConfirmationRole.WARNING, rh.gs(app.aaps.core.interfaces.R.string.bolus_recorded_only)) } if (useAlarm && !advisor && carbs > 0 && carbTime > 0) { - line(ConfirmationRole.INFO, rh.gs(app.aaps.core.ui.R.string.alarminxmin, carbTime)) + line(ConfirmationRole.INFO, rh.gs(app.aaps.core.interfaces.R.string.alarminxmin, carbTime)) } if (advisor) { - line(ConfirmationRole.INFO, rh.gs(app.aaps.core.ui.R.string.advisoralarm)) + line(ConfirmationRole.INFO, rh.gs(app.aaps.core.interfaces.R.string.advisoralarm)) } if (quickWizardEntry != null) { @@ -440,13 +440,13 @@ class BolusWizard @Inject constructor( val duration = JsonHelper.safeGetInt(quickWizardEntry.storage, "duration", 0) val carbs2 = JsonHelper.safeGetInt(quickWizardEntry.storage, "carbs2", 0) if (carbs2 > 0) { - val ecarbsMessage = rh.gs(app.aaps.core.ui.R.string.format_carbs, carbs2) + "/" + duration + "h (+" + timeOffset + "min)" - line(ConfirmationRole.INFO, rh.gs(app.aaps.core.ui.R.string.confirmation_line, rh.gs(app.aaps.core.ui.R.string.uel_extended_carbs), ecarbsMessage)) + val ecarbsMessage = rh.gs(app.aaps.core.interfaces.R.string.format_carbs, carbs2) + "/" + duration + "h (+" + timeOffset + "min)" + line(ConfirmationRole.INFO, rh.gs(app.aaps.core.interfaces.R.string.confirmation_line, rh.gs(app.aaps.core.interfaces.R.string.uel_extended_carbs), ecarbsMessage)) } } } if (eCarbsGrams > 0) { - line(ConfirmationRole.INFO, rh.gs(app.aaps.core.ui.R.string.wizard_ecarbs, eCarbsGrams, eCarbsDurationHours, eCarbsDelayMinutes)) + line(ConfirmationRole.INFO, rh.gs(app.aaps.core.interfaces.R.string.wizard_ecarbs, eCarbsGrams, eCarbsDurationHours, eCarbsDelayMinutes)) } } @@ -587,7 +587,7 @@ class BolusWizard @Inject constructor( bolus = detailedBolusInfo.createBolus(recordIcfg), action = action, source = source, - note = rh.gs(app.aaps.core.ui.R.string.record) + if (notes.isNotEmpty()) ": $notes" else "" + note = rh.gs(app.aaps.core.interfaces.R.string.record) + if (notes.isNotEmpty()) ": $notes" else "" ) } if (carbs > 0) { @@ -595,7 +595,7 @@ class BolusWizard @Inject constructor( carbs = detailedBolusInfo.createCarbs(), action = action, source = source, - note = notes.ifEmpty { rh.gs(app.aaps.core.ui.R.string.record) } + note = notes.ifEmpty { rh.gs(app.aaps.core.interfaces.R.string.record) } ) } persistenceLayer.insertOrUpdateBolusCalculatorResult(bolusCalculatorResult) @@ -656,7 +656,7 @@ class BolusWizard @Inject constructor( carbs = detailedBolusInfo.createCarbs(), action = Action.EXTENDED_CARBS, source = Sources.WizardDialog, - note = notes.ifEmpty { rh.gs(app.aaps.core.ui.R.string.record) } + note = notes.ifEmpty { rh.gs(app.aaps.core.interfaces.R.string.record) } ) } } else { diff --git a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt index 7d2f169b5163..534f5dd9bc71 100644 --- a/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt +++ b/core/objects/src/test/kotlin/app/aaps/core/objects/profile/ProfileSealedTest.kt @@ -65,10 +65,10 @@ class ProfileSealedTest : TestBase() { dateUtil = DateUtilImpl(context) hardLimits = HardLimitsMock(preferences, rh) whenever(activePlugin.activePump).thenReturn(testPumpPlugin) - whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_isf_units_mgdl))).thenReturn("mg/dL/U") - whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_isf_units_mmol))).thenReturn("mmol/L/U") - whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_carbs_per_unit))).thenReturn("g/U") - whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.ui.R.string.profile_ins_units_per_hour))).thenReturn("U/h") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.interfaces.R.string.profile_isf_units_mgdl))).thenReturn("mg/dL/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.interfaces.R.string.profile_isf_units_mmol))).thenReturn("mmol/L/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.interfaces.R.string.profile_carbs_per_unit))).thenReturn("g/U") + whenever(rh.gs(TextRef.AndroidRes(app.aaps.core.interfaces.R.string.profile_ins_units_per_hour))).thenReturn("U/h") whenever(rh.gs(anyInt(), anyString())).thenReturn("") whenever(activePlugin.activeAPS).thenReturn(aps) } diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt index 85b0d9bf178b..b3a1383de6f8 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/ProtectionHost.kt @@ -1,7 +1,6 @@ package app.aaps.core.ui.compose import androidx.compose.ui.res.stringResource -import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue diff --git a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt index 350a0d2276c2..b5edb3296adf 100644 --- a/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt +++ b/core/ui/src/androidMain/kotlin/app/aaps/core/ui/compose/preference/AdaptivePreferenceItem.kt @@ -5,7 +5,6 @@ package app.aaps.core.ui.compose.preference -import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/core/ui/src/androidMain/res/values-bg-rBG/strings.xml b/core/ui/src/androidMain/res/values-bg-rBG/strings.xml index 4f92ad504c07..e2dad4637ce3 100644 --- a/core/ui/src/androidMain/res/values-bg-rBG/strings.xml +++ b/core/ui/src/androidMain/res/values-bg-rBG/strings.xml @@ -38,8 +38,6 @@ Тип събитие мг/дл ммол/л - мг/дл/E - ммол/л/E %1$d мг/дл %1$.1f ммол/л Запиши @@ -105,18 +103,14 @@ Стари данни Ще стартира %1$.2fЕ болус AAPS стартирана - %1$+.2fЕ - %1$d гр %1$d+%2$d гр %1$.2f ч %1$d мин - %1$s: %2$s %1$s %2$s Цели Моля изчакайте... Стоп Натиснат СТОП - въглехидрати Грешен профил! НЕ Е ЗАДАДЕН ПРОФИЛ Не мога да сменя профил: няма избран инсулин. @@ -172,15 +166,12 @@ КЗ Калибрация CGM - Аларма след %1$d мин Помпата върна грешка. Проверете ръчно за реално доставените инсулин и въглехидрати Напомни ми за болус когато КЗ е нормална Продължителност гр. - Помпата е спряна Помпата е налична Не е конфигуриран - Loop изключен Кръгът е спрян поради смяна на времето нужн пада бързо @@ -230,7 +221,6 @@ Затворен кръг Отворен кръг Спиране на базал при ниска КЗ - Няма връзка с помпата Помпата е спряна Спри помпата Възстанови помпата @@ -266,11 +256,6 @@ Паролите не съвпадат ПИН кодовете не съвпадат - Базалните стойности не са за кръгли часове: %1$s - Базалната стойност е заместена от минимално поддържаната стойност: %1$s - Базалната стойност е заместена от максимално поддържаната стойност %1$s - Е/ч - гр/Е Стартирай профил %1$d%% за %2$d мин @@ -308,7 +293,6 @@ 30д - %1$d мин Careportal Проверка на КЗ @@ -492,7 +476,6 @@ Върни стандартната Кръг - Loop Найтскаут - Запиши В дясно под гърдите В ляво под гърдите Дясна ръка отстрани @@ -542,7 +525,6 @@ %1$d гр допълнителни въглехидрати ще са необходими до %2$d минути Базал - Болус ОДД Обща дневна доза @@ -554,7 +536,6 @@ УДЪЛЖЕН БОЛУС СУПЕРБОЛУС TBR (Временна базална доза) ВЪГЛЕХИДРАТИ - УДЪЛЖЕН БОЛУС ВРЕМЕНЕН БАЗАЛ ВРЕМЕННА ЦЕЛ НОВ ПРОФИЛ @@ -655,22 +636,15 @@ СЦЕНАРИЙ ДЕАКТИВИРАН ОТДАЛЕЧЕНАТА КОНФИГУРАЦИЯ Е СМЕНЕНА - Профил ниска цел - Профил висока цел Стойност цел минимум Стойност цел максимум Стойност временна цел - Профил време на действие на инсулин Време на действие на инсулин DIA Пик на инсулина - Профил чувствителност Профил макс базал Текущ базал - Профил въглехидратно число %1$.2f ограничен до %2$.2f »Стойността %1$s е извън ограниченията - »%1$s« %2$.2f е извън ограниченията - Базална стойност Няма избран профил Няма име на профила @@ -727,18 +701,12 @@ Съветник на болус Имаш висока захар. Вместо да се яде сега, се препоръчва да се изчака за по-добра захар. Искате ли да направите корекция сега и да ви напомня кога е време за ядене? В този случай няма да бъдат записвани въглехидрати и трябва да използвате съветника отново, когато ви напомня. - COB срещу IOB - !!!!! Засечено бавно усвояване на въглехидрати: %1$d%% от времето. Проверете калкулацията. Въглехидратите може би са прекалено много и това ще доведе до много инсулин !!!!! - Приложено болус ограничение: %1$.2f Е към %2$.2f Е - Само запиши (без да да го стартираш в помпата) - Аларма, когато е време за хранене КЗ Корекция Междинна сума Хляб, паста, кис мляко, плодове (ябълки, банани круши), картофи, ориз, бавни и нисковъглехидр храни, зеленчуци Паста, зърнени закуски, мед, сладка Лазаня, пица, хамбургери, пържени и печени картофи, чипс и др снаксове - Уд въглехидрати %1$dгр / %2$dч (+%3$dмин) Общо Започват от Сега @@ -983,7 +951,6 @@ Пон %1$.1f Е - %1$.2fЕ %1$+.2f Е Стойност: %1$.2f%% (%2$.2f Е/ч) Продължителност: %3$d мин Стойност: %1$.2f Е/ч (%2$.2f%%) Продължителност: %3$d мин diff --git a/core/ui/src/androidMain/res/values-ca-rES/strings.xml b/core/ui/src/androidMain/res/values-ca-rES/strings.xml index e9ee6d321df3..7731afea0acc 100644 --- a/core/ui/src/androidMain/res/values-ca-rES/strings.xml +++ b/core/ui/src/androidMain/res/values-ca-rES/strings.xml @@ -41,13 +41,10 @@ En pausa TDD Total A punt de lliurar %1$.2f U - %1$+.2f U - %1$d g %1$.2f h Objectius Espereu si us plau… Stop - Carbs ]]> Data Unitats @@ -82,12 +79,9 @@ Dades procedents d\'una altra bomba. Canvieu el driver de la bomba per restablir el seu estat. Glucèmia Calibració - Executar alarma en %1$d min Durada g - Bomba aturada No configurat - Llaç aturat req cap desconegut @@ -114,10 +108,6 @@ Contrasenya incorrecta Les contrasenyes no coincideixen - Valors basals no alineats amb les hores: %1$s - Valor basal reemplaçat pel màxim valor acceptat: %1$s - U/h - g/U Iniciar perfil %1$d%% durant %2$d min @@ -131,7 +121,6 @@ Mitja - %1$d min Portal de cures Control glucèmia @@ -170,7 +159,6 @@ Personalitzat Llaç NS - Registre Temps de connexió esgotat @@ -182,7 +170,6 @@ SMB Basal - Bolus Detecció de temps @@ -192,7 +179,6 @@ BOLUS ESTÈS SUPERBOLUS TBR CARBS - CARBS ESTESOS BASAL TEMP OBJECTIU TEMP NOU PERFIL @@ -274,20 +260,13 @@ LLAÇ MODIFICAT LLAÇ ELIMINAT - Objectiu baix del perfil - Objectiu alt del perfil Valor mínim d\'objectiu temporal Valor màxim d\'objectiu temporal Valor objectiu temporal - Valor DIA del perfil - Valor sensibilitat del perfil Valor basal màxima del perfil Valor basal actual - Valor ràtio de carbohidrats del perfil %1$.2f limitat a %2$.2f »%1$s« supera els límits - »%1$s« %2$.2f està fora dels límits estrictes - Valor basal BOLUS %1$.2f U @@ -307,8 +286,6 @@ Alarma urgent INFO - COB vs IOB - Executar alarma quan sigui hora de menjar Cap acció sel·leccionada, no passarà res Adolescent diff --git a/core/ui/src/androidMain/res/values-cs-rCZ/strings.xml b/core/ui/src/androidMain/res/values-cs-rCZ/strings.xml index 9c51af34c770..a181559e2cbf 100644 --- a/core/ui/src/androidMain/res/values-cs-rCZ/strings.xml +++ b/core/ui/src/androidMain/res/values-cs-rCZ/strings.xml @@ -38,8 +38,6 @@ Typ události mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Uložit @@ -103,18 +101,14 @@ Stará data Podávání %1$.2f U inzulínu AAPS spuštěno - %1$+.2f U - %1$d g %1$d+%2$dg %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Cíle Počkejte prosím… Stop STISKNUTO STOP - Sacharidy Neplatný profil! ŽÁDNÝ PROFIL NENASTAVEN ]]> @@ -164,15 +158,12 @@ Gly Kalibrace CGM - Spustit alarm za %1$d min Podání bolusu skončilo chybou. Ručně zkontrolujte, kolik inzulinu a sacharidů se skutečně vydalo Připomenout bolus při obnovení glykémie Trvání g - Pumpa pozastavena Pumpa běží Není nakonfigurováno - Smyčka pozastavena Smyčka pozastavena kvůli změně času pož rychle klesající @@ -222,7 +213,6 @@ Uzavřená smyčka Otevřená smyčka Ochrana před nízkou glykémií - Pumpa odpojena Pumpa pozastavena Pozastavit pumpu Obnovit pumpu @@ -258,11 +248,6 @@ Hesla se neshodují PIN kódy se neshodují - Hodnoty bazálů nejsou zarovnané na celé hodiny: %1$s - Hodnota bazálu byla nahrazena minimální možnou: %1$s - Hodnota bazálu nahrazena maximální možnou: %1$s - U/h - g/U Spustit profil %1$d%% na %2$d min @@ -300,7 +285,6 @@ 30d - %1$d min Péče Kontrola glykémie @@ -484,7 +468,6 @@ Vrátit na výchozí Smyčka NS - Záznam Hruď vpravo Hruď vlevo Vnější strana pravé paže nahoře @@ -534,7 +517,6 @@ Požadováno dalších %1$d g sacharidů během %2$d minut Bazál - Bolus CDD Celková denní dávka @@ -546,7 +528,6 @@ PRODLOUŽENÝ BOLUS SUPERBOLUS TBR SACHARIDY - ROZLOŽENÉ SACHARIDY DOČASNÝ BAZÁL DOČASNÝ CÍL NOVÝ PROFIL @@ -647,22 +628,15 @@ SCÉNA DEAKTIVOVÁNA VZDÁLENÁ KONFIGURACE ZMĚNĚNA - Dolní cíl profilu - Horní cíl profilu Dolní hodnota dočasného cíle Horní hodnota dočasného cíle Hodnota dočasného cíle - Hodnota DIA profilu Hodnota DIA inzulínu Hodnota vrcholu inzulínu - Hodnota citlivosti profilu Maximální hodnota profilu bazálu Aktuální hodnota bazálu - Inzulino-sacharidový poměr profilu %1$.2f omezeno na %2$.2f »%1$s« je mimo pevně nastavené limity - »%1$s« %2$.2f je mimo pevně nastavené limity - Hodnota bazálu Nebyl vybrán žádný profil Chybí název profilu @@ -719,18 +693,12 @@ Poradce pro bolus Máte vysokou glykémii. Namísto jídla doporučujeme vyčkat na lepší glykémii a připomenout, až bude čas na jídlo. Přejete si poslat korekční bolus a připomenout, až bude čas k jídlu? V tomto případě nebudou zapsané žádné sacharidy, a později opět musíte spustit kalkulátor, jakmile vám to připomeneme. - COB vs. IOB - !!!!! Byla zjištěna pomalá absorpce sacharidů: %1$d%% času. Zkontrolujte svůj výpočet. COB může být nadhodnoceno, takže by mohlo být podáno více inzulínu !!!!! - Použito omezení bolusu: %1$.2f U na %2$.2f U - Bolus nebude pumpou vydán, pouze zaznamenán - Spustit alarm, když je čas na jídlo Gly Korekce Mezisoučet Chléb, slané pečivo, jogurt, ovoce (jablko, banán, hruška), brambory, rýže, pohanka, ovesné vločky, pomalé a nízkosacharidové potraviny, zelenina Knedlíky, těstoviny, asijská kuchyně bohatá na sacharidy a tuky, sladké a kynuté pečivo, cereálie, obilné kaše, med, marmeláda Lasagne, pizza, hamburgery, hranolky, pečené brambory, chipsy a podobné svačiny - eSach %1$dg / %2$dh (+%3$dmin) Celkem Čas sacharidů Nyní @@ -975,7 +943,6 @@ Po %1$.1f U - %1$.2f U %1$+.2fU Hodnota: %1$.2f%% (%2$.2f U/h) Trvání: %3$d min Hodnota: %1$.2f U/h (%2$.2f%%) Trvání: %3$d min diff --git a/core/ui/src/androidMain/res/values-da-rDK/strings.xml b/core/ui/src/androidMain/res/values-da-rDK/strings.xml index 4d0c0717fc6a..109a9e5d073f 100644 --- a/core/ui/src/androidMain/res/values-da-rDK/strings.xml +++ b/core/ui/src/androidMain/res/values-da-rDK/strings.xml @@ -43,13 +43,10 @@ TDD Total Leverer %1$.2f IE AAPS startet - %1$+.2f IE - %1$d g %1$.2f t Mål Vent venligst… Stop - Kulhydrater Ugyldig profil! INGEN PROFIL SAT ]]> @@ -90,12 +87,9 @@ Data kommer fra en anden pumpe. Skift pumpedriver for at nulstille pumpetilstand. BG Kalibrering - Kør alarm om %1$d min Varighed g - Pumpe er afbrudt Ikke konfigureret - Loop suspenderet krævet falder hurtigt falder @@ -138,11 +132,6 @@ Kodeordene stemmer ikke overens PIN-koder ikke identiske - Basalværdier ikke angivet i hele timer: %1$s - Basal dosis erstattet af minimal understøttet dosis: %1$s - Basal dosis erstattet af maximal understøttet dosis: %1$s - E/t - g/E Start profil %1$d%% i %2$d min @@ -157,7 +146,6 @@ Gennemsnit - %1$d min Behandlingsportal BS kontrol @@ -200,7 +188,6 @@ Brugerdefineret Loop NS - Registrér Forbindelsen fik timeout @@ -213,7 +200,6 @@ %1$d g ekstra kulhydrater kræves inden for %2$d minutter Basal - Bolus Tidsdetektering @@ -223,7 +209,6 @@ FORLÆNGET BOLUS SUPERBOLUS TBR KH - FORLÆNGET KH MIDLERTIDIG BASAL MIDLERTIDIG MÅL NY PROFIL @@ -306,20 +291,13 @@ LOOP ÆNDRET LOOP FJERNET - Nedre målværdi for profilen - Øvre målværdi for profilen Nedre målværdi for midlertidig mål Øvre målværdi for midlertidig mål Midlertidig mål værdi - Profil DIA værdi - Profil følsomhed værdi Maksimal profil basal værdi Aktuel basal værdi - Profil KH ratio værdi %1$.2f begrænset til %2$.2f »%1$s« er uden for absolutte grænser - »%1$s« %2$.2f er uden for absolutte grænser - Basalværdi BOLUS %1$.2f IE @@ -354,10 +332,6 @@ Bolus-rådgiver Du har høj glycæmi. I stedet for at spise nu anbefales det at vente på bedre glycæmi. Ønsker du at lave en korrektionsbolus nu og minde dig om, hvornår det er tid til at spise? I dette tilfælde vil ingen kulhydrater blive registreret, og du skal bruge guiden igen, når vi påminder dig. - COB vs IOB - Bolus-begrænsning anvendt: %1$.2f IE til %2$.2f IE - Bolus registreres kun (bliver ikke leveret af pumpe) - Kør alarm når det er tid til at spise Ingen handling valgt, intet vil ske Ingen nylig BG til at basere beregningen på! Ingen aktiv profil angivet! @@ -469,7 +443,6 @@ Man %1$.1f IE - %1$.2f IE diff --git a/core/ui/src/androidMain/res/values-de-rDE/strings.xml b/core/ui/src/androidMain/res/values-de-rDE/strings.xml index de8fd698c888..0bbd0793ea4c 100644 --- a/core/ui/src/androidMain/res/values-de-rDE/strings.xml +++ b/core/ui/src/androidMain/res/values-de-rDE/strings.xml @@ -44,13 +44,10 @@ Veraltete Daten Werde %1$.2f IE abgeben AAPS gestartet - %1$+.2f IE - %1$d g %1$.2f h Objectives (Ziele) Bitte warten… Stopp - Kohlenhydrate Ungültiges Profil! KEIN PROFIL GESETZT ]]> @@ -91,12 +88,9 @@ Daten kommen von einer anderen Pumpe. Wechsle den Pumpentreiber. BZ Kalibrierung - Alarm in %1$d Min. Dauer g - Pumpe pausiert Nicht konfiguriert - Loop pausiert angef. schnell fallend fallend @@ -141,11 +135,6 @@ Die Passwörter stimmen nicht überein. PINs stimmen nicht überein - Basalraten beginnen nicht zur vollen Stunde: %1$s - Basal-Wert wurde durch den kleinst möglichen Wert ersetzt: %1$s - Basal-Wert wurde durch größt möglichen Wert ersetzt: %1$s - IE/h - g/IE Profil %1$d%% für %2$d Min. starten @@ -160,7 +149,6 @@ Durchschnitt - %1$d min. Careportal BZ-Test @@ -203,7 +191,6 @@ Benutzerdefiniert Loop NS - Eintrag Zeitüberschreitung bei Verbindung @@ -216,7 +203,6 @@ %1$d g zusätzliche Kohlenhydrate innerhalb von %2$d Minuten erforderlich Basal - Bolus Zeiterkennung @@ -226,7 +212,6 @@ VERLÄNGERTER BOLUS SUPERBOLUS TBR KOHLENHYDRATE - VERLÄNGERTE KOHLENHYDRATE TEMP BASAL TEMP. ZIEL NEUES PROFIL @@ -309,20 +294,13 @@ LOOP GEÄNDERT LOOP ENTFERNT - Profil unteres Ziel - Profil oberes Ziel Temp. Ziel unterer Wert Temp. Ziel oberer Wert Temp. Ziel Wert - Profil Insulinwirkdauer - Profil Sensitivitätswert Profil max. Basalwert Aktueller Basalwert - Profil KH-Faktor %1$.2f limitiert auf %2$.2f »%1$s« ist außerhalb der fest programmierten Grenzen - »%1$s« %2$.2f ist außerhalb der fest programmierten Grenzen - Basal-Wert BOLUS %1$.2f IE @@ -357,10 +335,6 @@ Bolus-Rechner Deine BZ-Werte sind hoch. Statt jetzt zu essen solltest Du abwarten, bis die Werte gesunken sind. Willst du jetzt einen Korrekturbolus abgeben und erinnert werden, wenn es Zeit zum Essen ist? In diesem Fall werden die Kohlenhydrate nicht übernommen und Du musst nach der Erinnerung den Bolus-Rechner erneut verwenden. - COB vs IOB - Bolus Einschränkung angewendet: %1$.2f U bis %2$.2f U - Bolus wird nur aufgezeichnet (Die Pumpe gibt kein Insulin ab!) - Alarmiere mich, wenn es Zeit zum Essen ist. Keine Aktion ausgewählt, nichts wird geschehen. Kein aktueller BG liegt als Basis zur Berechnung vor! Kein aktives Profil gesetzt! @@ -473,7 +447,6 @@ Mo %1$.1f IE - %1$.2f IE diff --git a/core/ui/src/androidMain/res/values-el-rGR/strings.xml b/core/ui/src/androidMain/res/values-el-rGR/strings.xml index b80c946327e8..d5c3e9aef591 100644 --- a/core/ui/src/androidMain/res/values-el-rGR/strings.xml +++ b/core/ui/src/androidMain/res/values-el-rGR/strings.xml @@ -43,13 +43,10 @@ TDD σύνολο Πρόκειται να εγχυθούν %1$.2f μονάδες Το AAPS ξεκίνησε - %1$+.2f U - %1$d g %1$.2f h Βήματα Περιμένετε… Stop - Υδατάνθρακες Μη έγκυρο προφίλ! ΔΕΝ ΟΡΙΣΤΗΚΕ ΠΡΟΦΙΛ ]]> @@ -90,12 +87,9 @@ Τα δεδομένα προέρχονται από διαφορετική αντλία. Αλλάξτε τον οδηγό της αντλίας για να επαναφέρετε την κατάσταση της αντλίας. BG Καλιμπράρισμα - Εκτέλεση συναγερμού σε %1$d λεπτά Διάρκεια g - Η αντλία είναι σε παύση Δεν έχει ρυθμιστεί - Κύκλωμα σε αναστολή req ταχεία πτώση πτώση @@ -138,11 +132,6 @@ Οι κωδικοί δεν ταιριάζουν Τα PINs δεν ταιριάζουν - Οι τιμές του βασικού ρυθμού δεν αντιστοιχούν σε ώρες: %1$s - Η τιμή του βασικού αντικαταστάθηκε από την ελάχιστη υποστηριζόμενη τιμή: %1$s - Η τιμή του βασικού αντικαταστάθηκε από την μέγιστη υποστηριζόμενη τιμή: %1$s - U/h - g/U Έναρξη προφίλ %1$d%% για %2$d λεπτά @@ -157,7 +146,6 @@ Μέση - %1$d λεπτά Φροντίδα Έλεγχος BG @@ -200,7 +188,6 @@ Προσαρμογή Κύκλωμα NS - Εγγραφή Ο χρόνος σύνδεσης έληξε @@ -213,7 +200,6 @@ %1$d g επιπλέον υδατάνθρακες απαιτούνται μέσα σε %2$d λεπτά Βασικός Ρυθμός - Bolus Ανίχνευση χρόνου @@ -223,7 +209,6 @@ ΕΚΤΕΤΑΜΕΝΟ BOLUS SUPERBOLUS TBR ΥΔΑΤΑΝΘΡΑΚΕΣ - ΕΚΤΕΤΑΜΕΝΟΙ ΥΔΑΤΑΝΘΡΑΚΕΣ ΠΡΟΣΩΡΙΝΟΣ ΒΑΣΙΚΟΣ ΠΡΟΣΩΡΙΝΟΣ ΣΤΟΧΟΣ ΝΕΟ ΠΡΟΦΙΛ @@ -306,20 +291,13 @@ ΤΟ ΚΥΚΛΩΜΑ ΑΛΛΑΞΕ ΤΟ ΚΥΚΛΩΜΑ ΑΦΑΙΡΕΘΗΚΕ - Χαμηλός στόχος προφίλ - Υψηλός στόχος προφίλ Κάτω τιμή προσωρινού στόχου Πάνω τιμή προσωρινού στόχου Τιμή προσωρινού στόχου - Τιμή DIA του προφίλ - Τιμή ευαισθησίας προφίλ Μέγιστη τιμή βασικού ρυθμού προφίλ Τρέχουσα τιμή βασικού - Τιμή αναλογίας υδατανθράκων προφίλ Το %1$.2f περιορίζεται σε %2$.2f » Το%1$s« είναι εκτός ορίων - Η τιμή »%1$s« %2$.2f είναι εκτός ορίων - Τιμή Βασικού ρυθμού BOLUS %1$.2f U @@ -354,10 +332,6 @@ Σύμβουλος Bolus Έχετε υπεργλυκαιμία. Αντί να φάτε τώρα συνιστάτε να περιμένετε μια καλύτερη γλυκαιμία. Θέλετε να κάνετε ένα διορθωτικό bolus τώρα και να γίνει υπενθύμιση όταν έρθει η ώρα να φάτε; Σε αυτή την περίπτωση δε θα καταγραφούν καθόλου υδατάνθρακες και θα πρέπει να χρησιμοποιήσετε τον οδηγό ξανά μετά την υπενθύμιση. - COB vs IOB (ενεργοί υδατάνθρακες vs ενεργή ινσουλίνη) - Ορίστηκε περιορισμός Bolus: %1$.2f U σε %2$.2f U - Το Bolus μόνο θα καταγραφεί (δε θα χορηγηθεί από την αντλία) - Εκτέλεση συναγερμού όταν έρθει η ώρα να φάτε Δεν έχει επιλεγεί καμία ενέργεια, δεν υπάρχει τίποτα να κάνει Δεν υπάρχει πρόσφατή γλυκόζη αίματος για να γίνει υπολογισμός! Δεν ορίστηκε ενεργό προφίλ! @@ -469,7 +443,6 @@ Δευτ %1$.1f U - %1$.2f U diff --git a/core/ui/src/androidMain/res/values-es-rES/strings.xml b/core/ui/src/androidMain/res/values-es-rES/strings.xml index de2986083af8..27cc7c9ce7ce 100644 --- a/core/ui/src/androidMain/res/values-es-rES/strings.xml +++ b/core/ui/src/androidMain/res/values-es-rES/strings.xml @@ -38,8 +38,6 @@ Tipo de evento mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Guardar @@ -105,18 +103,14 @@ Datos antiguos Entregando %1$.2f U AAPS iniciado - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Objetivos Por favor, espere… Detener PARADA PULSADA - Carbohidratos Perfil inválido NINGÚN PERFIL ACTIVO No se puede cambiar de perfil: no hay ninguna insulina en uso y no se ha seleccionado ninguna @@ -172,15 +166,12 @@ BG Calibración MCG - Ejecutar alarma en %1$d min El bolo reportó un error. Comprueba manualmente la cantidad real de insulina entregada y la cantidad de carbohidratos Recordar bolo cuando la glucemia se recupere Duración g - Bomba suspendida Bomba en funcionamiento Sin configurar - Lazo suspendido Lazo suspendido por DST (Horario de verano) req bajando rápido @@ -230,7 +221,6 @@ Lazo cerrado Lazo abierto Suspensión por glucosa baja (LGS) - Bomba desconectada Bomba suspendida Suspender bomba Reanudar bomba @@ -266,11 +256,6 @@ Las ontraseñas no coinciden Los códigos PIN no coinciden - Valores basales no alineados a las horas: %1$s - Valor basal cambiado al valor mínimo soportado: %1$s - Valor basal reemplazado por el valor máximo soportado: %1$s - U/h - g/U Iniciar perfil %1$d%% durante %2$d min @@ -308,7 +293,6 @@ 30d - %1$d min Portal de cuidados Medir glucosa @@ -492,7 +476,6 @@ Restablecer valores predeterminados Lazo NS - Registro Cadera derecha Cadera izquierda Brazo superior derecho, exterior @@ -542,7 +525,6 @@ %1$d g carbohidratos adicionales necesarios en %2$d minutos Dosis Basal - Bolo TDD Dosis Diaria Total @@ -554,7 +536,6 @@ BOLO EXTENDIDO SUPERBOLO TBR CARBOHIDRATOS - CARBOHIDRATOS EXTENDIDOS BASAL TEMPORAL OBJETIVO TEMPORAL NUEVO PERFIL @@ -655,22 +636,15 @@ ESCENA DESACTIVADA CAMBIO EN CONFIGURACIÓN REMOTA - Perfil de objetivo bajo - Perfil de objetivo alto Valor inferior objetivo temporal Valor superior objetivo temporal Valor objetivo temporal - Valor DIA del perfil Valor de DIA de la insulina Valor del pico de la insulina - Valor de sensibilidad del perfil Máximo valor basal del perfil Valor basal actual - Valor del ratio de carbohidratos del perfil %1$.2f limitado a %2$.2f »%1$s« está fuera del límite estricto - »%1$s« %2$.2f está fuera de los límites estrictos - Valor basal Ningún perfil seleccionado Falta el nombre del perfil @@ -727,18 +701,12 @@ Asistente de bolo Tienes la glucosa alta. En lugar de comer ahora, se recomienda esperar a tener un mejor valor de glucosa. ¿Quieres poner un bolo de corrección ahora y recibir un aviso cuando sea un buen momento para comer? En este caso no se registrarán los carbohidratos y deberás utilizar el asistente de bolos después de recibir la notificación. - COB vs IOB - ¡¡¡Absorción lenta de hidratos detectada: %1$d%% del tiempo. Revisa tus cálculos. Los COB pueden estar sobreestimados y el sistema podría darte demasiada insulina!!! - Restricción de bolo aplicada: %1$.2f U a %2$.2f U - El bolo sólo se anotará (no será entregado por la bomba) - Ejecutar alarma cuando sea hora de comer Glucemia Corrección Subtotal Pan, pastas saladas, yogurt, fruta (manzana, plátano, pera), patatas, arroz, trigo sarraceno, avena, alimentos de absorción lenta y bajos en carbohidratos, verduras Empanadillas, pasta, cocina asiática rica en hidratos y grasas, bollería dulce y de masa fermentada (cruasanes, brioches, donuts), cereales, gachas de cereales, miel, mermelada Lasaña, pizza, hamburguesas, patatas fritas, patatas de bolsa y snacks similares - eCarbs %1$dg / %2$dh (+%3$dmin) Total Hora de los carbohidratos Ahora @@ -983,7 +951,6 @@ Lun %1$.1f U - %1$.2f U %1$+.2f U Ratio: %1$.2f%% (%2$.2f U/h) Duración: %3$d min Ratio: %1$.2f U/h (%2$.2f%%) Duración: %3$d min diff --git a/core/ui/src/androidMain/res/values-fr-rFR/strings.xml b/core/ui/src/androidMain/res/values-fr-rFR/strings.xml index bf0ab3355827..9d22b0249dc8 100644 --- a/core/ui/src/androidMain/res/values-fr-rFR/strings.xml +++ b/core/ui/src/androidMain/res/values-fr-rFR/strings.xml @@ -38,8 +38,6 @@ Type d\'évènement mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Enregistrer @@ -106,18 +104,14 @@ %1$.2f U vont être injectées Bolus injecté, mais les glucides n\'ont pas pu être enregistrés. Veuillez entrer les glucides à nouveau. AAPS démarré - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Objectifs Merci de patienter... Arrêt ARRÊT APPUYÉ - Glucides Profil incorrect! PAS DE PROFIL SELECTIONNÉ Impossible de changer de profil : aucune insuline n\'est utilisée, et aucune insuline n\'a été sélectionnée. @@ -173,15 +167,12 @@ Gly Étalonnage MGC - Alerter dans %1$d min Erreur lors du Bolus. Vérifiez manuellement la quantité réellement injectée ainsi que les glucides actifs Rappeler le bolus quand la Gly remonte Durée g - Pompe arrêtée Pompe en fonctionnement Non configuré - La Boucle est suspendue Boucle suspendue par le changement d\'heure req en baisse rapide @@ -231,7 +222,6 @@ Boucle Fermée Boucle Ouverte Arrêt Glycémie Basse - Pompe déconnectée Pompe suspendue Suspendre la pompe Reprendre la pompe @@ -267,11 +257,6 @@ Mots de passe différents Les codes PIN ne correspondent pas - Valeurs des débits de basal non alignées sur des heures: %1$s - Valeur de basal remplacée par la valeur minimale autorisée : %1$s - Valeur de basal remplacée par la valeur maximale autorisée : %1$s - U/h - g/U Démarrer le profil %1$d%% pour %2$d min @@ -309,7 +294,6 @@ 30j - %1$d min Careportal Contrôle Glycémie @@ -493,7 +477,6 @@ Rétablir aux valeurs par défaut Boucle NS - Enregistrement Poitrine Droite Poitrine Gauche Bras Droit Extérieur @@ -543,7 +526,6 @@ %1$dg de glucides requis dans %2$d min. Basal - Bolus DTQ Dose Totale Quotidienne @@ -555,7 +537,6 @@ BOLUS ÉTENDU DBT SUPERBOLUS GLUCIDES - GLUCIDES ÉTENDUS BASAL TEMP CIBLE TEMP NOUVEAU PROFIL @@ -656,22 +637,15 @@ SCÉNARIO DÉSACTIVÉ CONFIG DISTANTE CHANGÉE - Cible basse du profil - Cible haute du profil Valeur basse de Cible Temp. Valeur haute de Cible Temp. Valeur de Cible Temporaire - Valeur DAI du profil Valeur DAI de l\'insuline Valeur du Pic de l\'insuline - Valeur de sensibilité du profil Basale maximale du profil Basale actuelle - Rapport glucides/insuline de profil %1$.2f limité à %2$.2f \"%1$s\" est en dehors des limites - \"%1$s\" %2$.2f est en dehors des limites - Valeur de Basal Aucun profil séléctionné Nom de profil manquant @@ -728,18 +702,12 @@ Assistant bolus Vous avez une glycémie élevée. Au lieu de manger maintenant, il est recommandé d\'attendre une meilleure glycémie. Voulez-vous faire un bolus de correction maintenant et avoir une alerte quand il sera temps de manger ? Dans ce cas, aucun glucide ne sera enregistré et vous devrez utiliser l\'assistant à nouveau lorsque nous vous le rappelons. - GA vs IA - !!!!! Absorption lente des glucides détectée dans %1$d%% des cas. Vérifiez de nouveau votre calcul. Les GA (Glucides Actifs) peuvent être surestimés et alors trop d\'insuline pourrait être injectée !!!!! - Contrainte de Bolus appliquée : %1$.2f U vers %2$.2f U - Les bolus seront seulement enregistrés (pas délivrés par la pompe) - Alerter quand il est temps de manger Gly Correction Sous-total Pain, viennoiseries, yaourts, fruits (pomme, banane, poire), pommes de terre, riz, sarrasin d\'avoine, aliments lents et à faible teneur en glucides, légumes Raviolis, pâtes, cuisine asiatique riche en glucides et graisses, pâtisseries sucrées et levures, céréales, porridge de céréales, miel, confiture Lasagne, pizza, hamburgers, frites, pommes de terre cuites, chips et collations similaires - eGlucides %1$d g / %2$d h (+%3$d min) Total Décalage horaire Maintenant @@ -984,7 +952,6 @@ Lun %1$.1f U - %1$.2f U %1$+.2f U Débit: %1$.2f%% (%2$.2f U/h) Durée: %3$d min Débit: %1$.2f U/h (%2$.2f%%) Durée: %3$d min diff --git a/core/ui/src/androidMain/res/values-hr-rHR/strings.xml b/core/ui/src/androidMain/res/values-hr-rHR/strings.xml index 566360fb9db2..f4e55b5b63c8 100644 --- a/core/ui/src/androidMain/res/values-hr-rHR/strings.xml +++ b/core/ui/src/androidMain/res/values-hr-rHR/strings.xml @@ -33,13 +33,10 @@ TDD Total Isporučit ću %1$.2f U AAPS je počeo - %1$+.2f U - %1$d g %1$.2f h Ciljevi Molimo pričekajte… Zaustavi - UH ]]> Datum Jedinice @@ -60,10 +57,8 @@ Aktivni UGH GUK Kalibracija - Uključi alarm za %1$d min Trajanje g - Petlja suspendirana potrebno brzo padajući padanje @@ -96,11 +91,6 @@ Lozinke se ne podudaraju PIN-ovi se ne podudaraju - Bazalne vrijednosti nisu usklađene sa satima: %1$s - Bazalna vrijednost zamijenjena minimalnom podržanom vrijednošću: %1$s - Bazalna vrijednost zamijenjena maksimalnom podržanom vrijednošću: %1$s - U/h - g/U Pokrenite profil %1$d%% na %2$d min @@ -128,7 +118,6 @@ %1$d g dodatnih ugljikohidrata potrebnih unutar %2$d minuta Bazal - Bolus Detekcija vremena @@ -172,16 +161,11 @@ PETLJA PROMIJENJENA PETLJA UKLONJENA - Profil niski cilj - Profil visoki cilj Privremena ciljana donja vrijednost Privremena ciljna gornja vrijednost Privremena ciljna vrijednost - Profil DIA vrijednost - Vrijednost osjetljivosti profila Maksimalna bazalna vrijednost profila Trenutna bazalna vrijednost - Vrijednost omjera ugljikohidrata profila %1$.0f%% diff --git a/core/ui/src/androidMain/res/values-hu-rHU/strings.xml b/core/ui/src/androidMain/res/values-hu-rHU/strings.xml index 1766ccf5d1a2..8619d5373d0e 100644 --- a/core/ui/src/androidMain/res/values-hu-rHU/strings.xml +++ b/core/ui/src/androidMain/res/values-hu-rHU/strings.xml @@ -15,7 +15,6 @@ Pumpa Némít - %1$+.2f E Kérem várjon… Leállít DIA @@ -54,8 +53,6 @@ Hibás jelszó Jelszavak nem egyeznek - E/ó - g/E Időtartam @@ -80,7 +77,6 @@ Bázis - Bólus BÓLUS MEGSZAKÍT @@ -146,7 +142,6 @@ Ke - %1$.2f E diff --git a/core/ui/src/androidMain/res/values-it-rIT/strings.xml b/core/ui/src/androidMain/res/values-it-rIT/strings.xml index 7ec143ad76b1..fce6e5608bf9 100644 --- a/core/ui/src/androidMain/res/values-it-rIT/strings.xml +++ b/core/ui/src/androidMain/res/values-it-rIT/strings.xml @@ -38,8 +38,6 @@ Tipo evento mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Salva @@ -103,18 +101,14 @@ Dati vecchi Sto per erogare %1$.2f U AAPS avviato - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Obiettivi Attendi… Stop STOP PREMUTO - CHO Profilo non valido! NESSUN PROFILO IMPOSTATO ]]> @@ -164,15 +158,12 @@ BG Calibrazione CGM - Esegui allarme in %1$d min Segnalato errore sul bolo. Controlla manualmente la reale quantità erogata Ricorda il bolo quando la glicemia recupera Durata g - Micro sospeso Micro in esecuzione Non configurato - Loop sospeso Loop sospeso da DST ric discesa rapida @@ -222,7 +213,6 @@ Loop chiuso Loop aperto Sospensione a glicemia bassa (LGS) - Micro disconnesso Micro sospeso Sospendi micro Riprendi micro @@ -258,11 +248,6 @@ Le password non coincidono I PIN non coincidono - Valori basali non allineati alle ore: %1$s - Valore basale sostituito dal minimo valore supportato: %1$s - Valore basale sostituito dal massimo valore supportato: %1$s - U/h - g/U Avvia profilo %1$d%% per %2$d min @@ -300,7 +285,6 @@ 30gg - %1$d min Portale Controllo BG @@ -484,7 +468,6 @@ Ripristino delle impostazioni predefinite Loop NS - Record Fianco destro Fianco sinistro Braccio Esterno Superiore Destro @@ -534,7 +517,6 @@ %1$d g di CHO aggiuntivi richiesti entro %2$d minuti Basale - Bolo TDD Dose totale giornaliera @@ -546,7 +528,6 @@ BOLO ESTESO TBR SUPERBOLO CHO - CHO ESTESI BASALE TEMPORANEA TARGET TEMPORANEO NUOVO PROFILO @@ -647,22 +628,15 @@ SCENA DISATTIVATA CONFIG REMOTA MODIFICATA - Target basso (profilo) - Target alto (profilo) Valore inferiore target temporaneo Valore superiore target temporaneo Valore target temporaneo - Valore DIA (profilo) Valore del DIA dell’insulina Valore di picco dell\'insulina - Valore sensibilità (profilo) Valore max basale (profilo) Valore basale corrente - Valore rapporto CHO (profilo) %1$.2f limitato a %2$.2f »%1$s« è fuori dai limiti consentiti - »%1$s« %2$.2f è fuori dai limiti consentiti - Valore basale Nessun profilo selezionato Nome profilo mancante @@ -719,18 +693,12 @@ Consiglio bolo Hai una glicemia alta. Invece di mangiare ora, si consiglia di attendere una glicemia migliore. Vuoi fare adesso un bolo di correzione ed ricevere un promemoria quando è il momento di mangiare? In questo caso non verranno registrati carboidrati e dovrai usare di nuovo il calcolatore quando ti verrà mostrato il promemoria. - COB vs IOB - !!!!! Rilevato assorbimento lento dei carboidrati: %1$d%% del tempo. Ricontrolla il tuo calcolo. I COB potrebbero essere sovrastimati e potrebbe essere somministrata più insulina !!!!! - Vincolo bolo applicato: %1$.2f U a %2$.2f U - Il bolo sarà solo registrato (non erogato dal micro) - Esegui allarme quando è tempo di mangiare BG Correzione Subtotale Pane, pasticcini salati, yogurt, frutta (mela, banana, pera), patate, riso, grano saraceno, farina d\'avena, alimenti lenti e a basso contenuto di carboidrati, verdure Ravioli, pasta, cucina asiatica ricca di carboidrati e grassi, dolci dolci e lieviti, cereali, porridge di grano, miele, marmellata Lasagna, pizza, hamburger, patatine fritte, patate, chips e snack simili - eCarbs %1$dg / %2$dh (+%3$dmin) Totale Tempo Carbo Adesso @@ -969,7 +937,6 @@ Lun %1$.1f U - %1$.2f U %1$+.2f U Velocità: %1$.2f%% (%2$.2f U/h) Durata: %3$d min Velocità: %1$.2f U/h (%2$.2f%%) Durata: %3$d min diff --git a/core/ui/src/androidMain/res/values-iw-rIL/strings.xml b/core/ui/src/androidMain/res/values-iw-rIL/strings.xml index 0194e62cb9e2..ec7fee90b6f3 100644 --- a/core/ui/src/androidMain/res/values-iw-rIL/strings.xml +++ b/core/ui/src/androidMain/res/values-iw-rIL/strings.xml @@ -44,13 +44,10 @@ נתונים ישנים עומד להזריק %1$.2f יח\' הופעל AndroidAPS - %1$+.2f יח\' - %1$d גר\' %1$.2f ש\' משימות נא להמתין… עצור - פחמימות פרופיל לא חוקי! לא הוגדר פרופיל ]]> @@ -91,12 +88,9 @@ הנתונים מגיעים ממשאבה אחרת. בחרו מחדש את סוג המשאבה כדי לאפס את מצב המשאבה. BG כיול - הפעל התראה בעוד %1$d דקות משך גר\' - משאבה מושהית לא מוגדר - לולאה מושהית נדרשים ירידה מהירה ירידה @@ -143,11 +137,6 @@ הסיסמאות אינן תואמות קודי PIN אינם תואמים - ערכי הבזאלי לא מותאמים לשעות: %1$s - ערכי הבזאלי הוחלפו בערכים הנתמכים המינימליים: %1$s - ערכי הבזאלי הוחלפו בערכים הנתמכים המינימליים: %1$s - יח\'\\שעה - גר\'\\יח\' הפעלת פרופיל %1$d%% במשך %2$d דק\' @@ -162,7 +151,6 @@ ממוצע - %1$d דק\' פורטל הטיפולים בדיקת רמת סוכר בדם @@ -205,7 +193,6 @@ מותאם אישית לולאה נייטסקאוט - הקלטה החיבור חרג ממגבלת הזמן @@ -218,7 +205,6 @@ %1$d גר\' פחמימות דרושות ב-%2$d הדקות הקרובות בזאלי - בולוס זיהוי שעה @@ -228,7 +214,6 @@ בולוס ממושך בזאלי זמני של סופר בולוס פחמימות - פחמימות מורכבות בזאלי זמני ערך מטרה זמני פרופיל חדש @@ -313,20 +298,13 @@ הלולאה שונתה הלולאה הוסרה - ערך המטרה הנמוך של הפרופיל - ערך המטרה הגבוה של הפרופיל ערך תחתון של המטרה הזמנית ערך עליון של המטרה הזמנית ערך המטרה הזמני - ערך DIA של הפרופיל - ערך הרגישות של הפרופיל מינון בזאלי מרבי של הפרופיל מינון בזאלי נוכחי - יחס הפחמימות של הפרופיל %1$.2f מוגבל ל- %2$.2f »הערך %1$s« מחוץ לתחום הקשיח - »הערך %1$s« %2$.2f מחוץ לתחום הקשיח - ערך בזאלי בולוס %1$.2f יח\' @@ -364,10 +342,6 @@ יועץ הבולוסים יש לכם היפרגליקמיה. במקום לאכול עכשיו מומלץ לחכות לרמת סוכר נמוכה יותר. האם תרצו לעשות בולוס תיקון עכשיו ולאחר מכן תופעל תזכורת לאכול? במקרה זה, לא יירשמו פחמימות ויהיה עליכם להשתמש באשף מחדש כאשר תגשו לאכול. - פחמ\' פעילות לעומת אינ\' פעיל - מגבלת בולוס יושמה: %1$.2f עד %2$.2f יח\' - בולוס רשום בלבד (לא מוזרק על ידי המשאבה) - הפעל התראה כשצריכים לאכול לא נבחרה פעולה, דבר לא יתבצע. אין נתוני סוכר לביסוס חישוב! לא הופעל פרופיל! @@ -489,7 +463,6 @@ ב\' %1$.1f יח\' - %1$.2f יח\' diff --git a/core/ui/src/androidMain/res/values-ko-rKR/strings.xml b/core/ui/src/androidMain/res/values-ko-rKR/strings.xml index c24cc42d1b07..0cb84f47e64a 100644 --- a/core/ui/src/androidMain/res/values-ko-rKR/strings.xml +++ b/core/ui/src/androidMain/res/values-ko-rKR/strings.xml @@ -44,13 +44,10 @@ 오래된 데이터 %1$.2f U을 주입합니다. AAPS 시작 - %1$+.2f U - %1$d g %1$.2f 시간 목표 잠시 기다려 주세요... 정지 - 탄수화물 유효하지 않은 프로파일! 프로파일 설정되지 않음 ]]> @@ -91,12 +88,9 @@ 다른 펌프에서 전송된 데이터. 펌프 상태 재설정을 위해 펌프 드라이버를 바꾸세요. 혈당 보정 - %1$d분 뒤 알람 울림 기간 g - 펌프 일시중지됨 설정되지 않음 - Loop 일시중지 필요량 급속 하락 하락 @@ -141,11 +135,6 @@ 비밀번호가 일치하지 않습니다. PIN이 일치하지 않습니다 - Basal값이 시간단위로 설정되지 않았습니다: %1$s - 지원되는 최대 값으로 Basal 값이 대체되었습니다: %1$s - 지원되는 최대값으로 Basal값이 대체되었습니다:%1$s - U/h - g/U 프로파일 %1$d%%을 %2$d 분 동안 시작 @@ -160,7 +149,6 @@ 평균 - %1$d 분 케어포털 혈당 체크 @@ -203,7 +191,6 @@ 사용자 정의 Loop NS - 기록 연결시간초과 @@ -216,7 +203,6 @@ %2$d 분 내에 %1$d g의 추가적인 탄수화물이 필요함 Basal - Bolus 시간 감지 @@ -226,7 +212,6 @@ 확장 bolus Superbolus TBR 탄수화물 - 확장 탄수화물 임시 basal 임시 목표 새로운 프로파일 @@ -309,20 +294,13 @@ Loop 변경됨 Loop 제거됨 - 프로파일 저혈당 목표 - 프로파일 고혈당 목표 임시 목표 최저값 임시 목표 최고값 임시 목표 수치 - 프로파일 DIA 값 - 프로파일 민감도 값 프로파일의 최대 basal 값 현재 basal 값 - 프로파일의 탄수화물 비율 값 %1$.2f은 %2$.2f까지 제한됨 »%1$s«이 \'고정된 한계값\'을 벗어났습니다. - »%1$s« %2$.2f이 \'고정된 한계값\'을 벗어났습니다. - Basal 값 BOLUS %1$.2f U @@ -357,10 +335,6 @@ Bolus 조언자 현재 높은 혈당을 가지고 있습니다. 지금 먹는 대신 더 나은 혈당을 기다리는 게 좋습니다. 지금 교정bolus 을 진행하고, 식사 시간을 알려드릴까요? 이 경우 탄수화물은 기록되지 않으며, 다시 알림을 줄 때 마법사를 다시 사용해야 합니다. - COB vs IOB - 적용된 Bolus 제약 조건: %1$.2f U 에서 %2$.2f U - Bolus는 이 경우(펌프를 통해 공급되지 않음) 에만 기록됩니다 - 식사 시간이 되면 알람을 울리기 선택한 실행이 없습니다. 아무런 실행이 되지 않습니다. 기저 계산에는 최근 BG가 없습니다! 활성화된 프로파일 설정이 없습니다! @@ -469,7 +443,6 @@ %1$.1f U - %1$.2f U diff --git a/core/ui/src/androidMain/res/values-lt-rLT/strings.xml b/core/ui/src/androidMain/res/values-lt-rLT/strings.xml index b49b47c0fbc7..f43c198bfd9d 100644 --- a/core/ui/src/androidMain/res/values-lt-rLT/strings.xml +++ b/core/ui/src/androidMain/res/values-lt-rLT/strings.xml @@ -44,13 +44,10 @@ Seni duomenys Bus suleista %1$.2f vv AAPS paleista - %1$+.2f vv - %1$dg %1$.2f val. Tikslai Palaukite… Stop - Angliavandeniai Netinkamas profilis! Nenustatytas profilis ]]> @@ -92,13 +89,10 @@ Duomenys gaunami iš kitos pompos. Pakeiskite pompos valdiklį. Glikemija Kalibravimas - Pranešti po %1$d min Boluso suleidimo klaida. Patikrinkite faktiškai suleistą insulino kiekį ir įvestą angliavandenių skaičių Trukmė g - Pompa sustabdyta Nesukonfigūruota - Ciklas sustabdytas reikal. greitai mažėja mažėja @@ -151,11 +145,6 @@ Slaptažodžiai nesutampa PIN kodai nesutampa - Bazės reikšmės nesuderintos su valandomis: %1$s - Nustatyta mažiausia galima valandinės bazės vertė: %1$s - Nustatyta didžiausia galima valandinės bazės vertė: %1$s - v/val - g/v Pradėti profilį %1$d%% %2$d min @@ -170,7 +159,6 @@ Vidurkis - %1$d min. Priežiūra Gliukomatis @@ -215,7 +203,6 @@ Pasirinktinis Ciklas NS - Įrašas Prijungimo laikas baigėsi @@ -228,7 +215,6 @@ Būtina suvartoti %1$d g papildomų AV per %2$d min Valandinė bazė - Bolusas Laiko nustatymas @@ -238,7 +224,6 @@ IŠTĘSTAS BOLUSAS SUPERBOLUSO LB AV - IŠTĘSTI AV LAIKINA BAZĖ LAIKINAS TIKSLAS NAUJAS PROFILIS @@ -323,20 +308,13 @@ CIKLAS PAKEISTAS CIKLAS PAŠALINTAS - Profilio tikslo žemoji riba - Profilio tikslo aukštoji riba Laikino tikslo žemoji riba Laikino tikslo viršutinė riba Laikino tikslo reikšmė - Profilio IVT reikšmė - Profilio JIF reikšmė Maksimali profilio VB reikšmė Dabartinė bazė - Profilio IA reikšmė %1$.2f apribota iki %2$.2f »%1$s« viršija griežtą limitą - »%1$s« %2$.2f viršija griežtą limitą - Valandinė bazė BOLUSAS %1$.2f vv @@ -374,10 +352,6 @@ Boluso patarėjas Jūsų glikemija yra aukšta. Užuot valgę dabar, turėtumėte palaukti, kol sumažės glikemija. Ar norite dabar susileisti korekcinį bolusą ir nustatyti priminimą, kada ateis laikas valgyti? Tokiu atveju angliavandeniai nebus įrašyti, o po priminimo turėsite vėl naudoti boluso skaičiuoklę. - AAO prieš AIO - Pritaikytas boluso apribojimas: %1$.2f v iki %2$.2f v - Bolusas bus tik įrašytas (nebus suleistas) - Pranešti apie laiką valgyti Veiksmas nepasirinktas, nieko neįvyks Nėra naujausių cukraus duomenų, kuriais būtų galima pagrįsti skaičiavimus! Neparinktas aktyvus profilis! @@ -507,7 +481,6 @@ P %1$.1f V - %1$.2f V Dydis: %1$.2f%% (%2$.2f vv/val) Trukmė: %3$d min Dydis: %1$.2f vv/val. (%2$.2f%%) Trukmė: %3$d min Dydis
: %1$.2f%% (%2$.2f vv/val.)
Trukmė: %3$d min
]]>
diff --git a/core/ui/src/androidMain/res/values-nb-rNO/strings.xml b/core/ui/src/androidMain/res/values-nb-rNO/strings.xml index 29c732489069..cb12b4db25b0 100644 --- a/core/ui/src/androidMain/res/values-nb-rNO/strings.xml +++ b/core/ui/src/androidMain/res/values-nb-rNO/strings.xml @@ -38,8 +38,6 @@ Hendelsestype mg/dL mmol/L - mg/dL/E - mmol/L/E %1$d mg/dL %1$.1f mmol/L Lagre @@ -106,18 +104,14 @@ Leverer %1$.2f enheter Bolusen ble levert, men karbohydratene kunne ikke lagres. Registrer karbo på nytt. AAPS startet - %1$+.2f E - %1$d g %1$d+%2$d g %1$.2f t %1$d min - %1$s: %2$s %1$s %2$s Opplæringsmål Vennligst vent… Stopp STOPP TRYKKET - Karbo Ugyldig profil! INGEN PROFIL VALGT Kan ikke bytte profil: Ingen insulin er i bruk, og ingen er valgt. @@ -173,15 +167,12 @@ BS Kalibrering Sensor - Aktiver alarm om %1$d min Det er registrert en feil med boluslevering. Sjekk manuelt om den er levert og hvor mye Påminnelse om bolus når BS kommer seg igjen Varighet g - Pumpen er pauset Pumpe i drift Ikke konfigurert - Loop pauset Loop suspendert av DST (Sommertid) nødv synker raskt @@ -231,7 +222,6 @@ Lukket Loop Åpen Loop Stopp ved lavt BS - Pumpe frakoblet Pumpe pauset Pause pumpe Gjenoppta pumpe @@ -267,11 +257,6 @@ Passord stemmer ikke overens PIN-kodene samsvarer ikke - Basalverdier er ikke angitt på hele timer: %1$s - Basalverdi erstattet med minste tillate verdi: %1$s - Basalverdi erstattet med høyeste tillate verdi: %1$s - E/t - g/E Start profil %1$d%% i %2$d min @@ -309,7 +294,6 @@ 30d - %1$d min Helseportal BS-kontroll @@ -493,7 +477,6 @@ Tilbakestill til standardinnstillinger Loop NS - Registrer Høyre bryst Venstre bryst Øvre høyre ytterside av armen @@ -543,7 +526,6 @@ %1$d g ekstra karbohydrater kreves innen %2$d minutter Basal - Bolus TDD Total daglig dose @@ -555,7 +537,6 @@ FORLENGET BOLUS SUPERBOLUS TBR KARBO - FORLENGET KARBO MIDL. BASAL MIDLERTIDIG MÅL NY PROFIL @@ -656,22 +637,15 @@ SCENE DEAKTIVERT FJERNKONFIGURASJON ENDRET - Profil lavt mål - Profil høyt mål Nedre grense for midlertidig mål Øvre grense for midlertidig mål Midlertidig målverdi - Profil DIA verdi Insulin DIA-verdi Insulin toppverdi - Profilens insulinfølsomhet Maksimal profil basalverdi Nåværende basalverdi - Profilens insulin-til-karbohydratforhold (IK) %1$.2f begrenset til %2$.2f »%1$s« er utenfor lovlige grenseverdier - »%1$s« %2$.2f er utenfor lovlige grenseverdier - Basalverdi Ingen profil valgt Mangler profilnavn @@ -728,18 +702,12 @@ Bolusveiviser Du har høyt blodsukker. I stedet for å spise nå er det bedre å utsette det til du har et lavere blodsukker. Ønsker du å sette en korreksjonsbolus nå og få en påminnelse om når det er på tide å spise? I dette tilfellet vil ingen karbohydrater registreres nå, og du må bruke boluskalkulatoren igjen når vi gir deg en påminnelse. - COB vs IOB - !!!!! Langsom karboabsorpsjon oppdaget: %1$d%% av tiden. Dobbeltsjekk beregningen din. COB kan overestimeres, og dermed kan mer insulin gis !!!!! - Bolus begrensning brukt: %1$.2f E til %2$.2f E - Bolus vil bare bli loggført (ikke levert av pumpe) - Aktiver alarm når det er på tide å spise BS Korreksjon Delsum Brød, bakverk, yoghurt, frukt (eple, banan, pære), poteter, ris, bokhvete, havregryn, langsom og lavkarbo mat, grønnsaker Dumplings, pasta, asiatisk mat rik på karbohydrater og fett, søt gjærbakst, frokostblandinger, grøt med korn, honning, syltetøy Lasagne, pizza, hamburgere, pommes frites, bakte poteter, chips og lignende snacks - eKarbo %1$dg / %2$dt (+%3$dmin) Total Karbotid @@ -984,7 +952,6 @@ Man %1$.1f E - %1$.2f E %1$+.2f E Dose: %1$.2f%% (%2$.2f E/t) Varighet: %3$d min Dose: %1$.2f E/t (%2$.2f%%) Varighet: %3$d min diff --git a/core/ui/src/androidMain/res/values-nl-rNL/strings.xml b/core/ui/src/androidMain/res/values-nl-rNL/strings.xml index 3943f84d1429..d0fbed7112f5 100644 --- a/core/ui/src/androidMain/res/values-nl-rNL/strings.xml +++ b/core/ui/src/androidMain/res/values-nl-rNL/strings.xml @@ -44,13 +44,10 @@ Oude gegevens %1$.2f E toedienen AAPS gestart - %1$+.2f E - %1$d g %1$.2f u Doelen Even geduld a.u.b.… Stop - Koolhydraten Ongeldig profiel! GEEN PROFIEL INGESTELD ]]> @@ -94,14 +91,11 @@ Data komt van een andere pomp. Wijzig de pomp driver om de pomp status te resetten. BG Kalibratie - Start alarm over %1$d min Bolus fout geconstateerd. Controleer handmatig de werkelijk toegediende hoeveelheid insuline en koolhydraten Tijdsduur g - Pomp onderbreken Pomp is actief Niet ingesteld - Loop pauzeren Loop onderbroken voor zomer/wintertijd (DST) nodig snel dalend @@ -134,7 +128,6 @@ Closed loop Open loop Stop bij laag - Pomp niet verbonden Pomp is onderbroken Modus teruggezet DIA @@ -160,11 +153,6 @@ Wachtwoorden komen niet overeen PIN-codes komen niet overeen - Basaalstanden niet ingesteld in hele uren: %1$s - Minimum basaalwaarde is vervangen door minimaal ondersteunde waarde: %1$s - Basale waarde vervangen door maximale ondersteunde waarde: %1$s - E/u - g/E Start profiel %1$d%% voor %2$d min @@ -179,7 +167,6 @@ Gemiddelde - %1$d min Zorgportaal BG Controle @@ -227,7 +214,6 @@ Aangepast Loop NS - Opnemen Borst rechts Borst links Rechter bovenarm @@ -273,7 +259,6 @@ %1$d g extra koolhydraten nodig binnen %2$d minuten Basaal - Bolus Tijd detectie @@ -283,7 +268,6 @@ VERLENGDE BOLUS SUPERBOLUS TBR KOOLHYDRATEN - VERLENGDE KOOLHYDRATEN TIJDELIJK BASAAL TIJDELIJK DOEL NIEUW PROFIEL @@ -375,20 +359,13 @@ BEDRIJFSMODE VERWIJDERD BEDRIJFSMODE GEWIJZIGD - Profiel laag doel - Profiel hoog doel Tijdelijk streefdoel ondergrens Tijdelijk streefdoel bovengrens Tijdelijk streefdoel waarde - Profiel DIA waarde - Profiel gevoeligheidswaarde Maximale basaal waarde van het profiel Huidige basaal waarde - Profiel koolhydraten ratio waarde %1$.2f gelimiteerd tot %2$.2f »%1$s« is buiten de harde limiet - »%1$s« %2$.2f is buiten de harde limiet - Basaal waarde BOLUS %1$.2f E @@ -426,10 +403,6 @@ Bolusadviseur Je hebt een hoge bloedglucose. In plaats van te eten, is het nu aan te raden om te wachten op een betere bloedglucose. Wil je nu een correctiebolus uitvoeren en je laten weten wanneer het tijd is om te eten? In dit geval worden er geen koolhydraten opgenomen en moet je de wizard opnieuw gebruiken wanneer we je er aan herinneren. - COB vs IOB - Bolusbeperking toegepast: %1$.2f E naar %2$.2f E - Bolus wordt alleen geregistreerd (niet toegediend door pomp) - Start alarm wanneer het tijd is om te eten Geen actie geselecteerd, er zal niets uitgevoerd worden Geen recente BG om de berekening op te baseren! Geen actief profiel ingesteld! @@ -553,7 +526,6 @@ Ma %1$.1f E - %1$.2f E Basaal: %1$.2f%% (%2$.2f E/h) Duur: %3$d min Basaal: %1$.2f E/h (%2$.2f%%) Duur: %3$d min Basaal
: %1$.2f%% (%2$.2f E/h)
Duur: %3$d min
]]>
diff --git a/core/ui/src/androidMain/res/values-pl-rPL/strings.xml b/core/ui/src/androidMain/res/values-pl-rPL/strings.xml index c0ff8c2a2060..1943c48cbc4e 100644 --- a/core/ui/src/androidMain/res/values-pl-rPL/strings.xml +++ b/core/ui/src/androidMain/res/values-pl-rPL/strings.xml @@ -44,13 +44,10 @@ Stare dane Zamierzam podać %1$.2f U AAPS uruchomiony - %1$+.2f U - %1$d g %1$.2f h Zadania Proszę czekać… Stop - Węglowodany Nieprawidłowy profil! NIE USTAWIONO PROFILU ]]> @@ -94,14 +91,11 @@ Dane pochodzą z innej pompy. Zmień sterownik pompy, aby zresetować stan pompy. BG Kalibracja - Uruchom alarm za %1$d min Błąd podczas podawania bolusa. Sprawdź ręcznie ile faktycznie podano insuliny i ilość węglowodanów Czas trwania g - Pompa wstrzymana Pompa uruchomiona Nie skonfigurowano - Pętla wstrzymana Pętla zawieszona przez zmianę czasu (na letni lub zimowy) wym szybko spada @@ -134,7 +128,6 @@ Zamknięta pętla Otwarta pętla Zawieszenie przy niskiej glikemii - Pompa odłączona Pompa wstrzymana Tryb przywrócony DIA @@ -160,11 +153,6 @@ Hasła się nie zgadzają Kody PIN nie pasują do siebie - Wartości bazy nie są ustawione w pełnych godzinach: %1$s - Wartość bazy zastąpiona minimalną obsługiwaną wartością: %1$s - Wartość bazy zastąpiona maksymalną obsługiwaną wartością: %1$s - U/h - g/U Uruchom profil %1$d%% na %2$d min @@ -179,7 +167,6 @@ Średnio - %1$d min PortalOpieki Sprawdź BG @@ -227,7 +214,6 @@ Niestandardowe Pętla NS - Wpis Prawa strona klatki Lewa strona klatki Górny bok prawego ramienia @@ -273,7 +259,6 @@ Zalecane podanie %1$d g węglowodanów w przeciągu %2$d minut Baza - Bolus Wykrywanie czasu @@ -283,7 +268,6 @@ BOLUS PRZEDŁUŻONY SUPERBOLUS TBR WĘGLOWODANY - PRZEDŁUŻONE WĘGLOWODANY BAZA TYMCZASOWA CEL TYMCZASOWY NOWY PROFIL @@ -375,20 +359,13 @@ USUNIĘTO TRYB DZIAŁANIA ZAKTUALIZOWANO TRYB DZIAŁANIA - Dolna granica celu profilu - Górna granica celu profilu Dolna wartość celu tymczasowego Górna wartość celu tymczasowego Wartość celu tymczasowego - Wartość DIA profilu - Wartość wrażliwości profilu Maksymalna wartość bazowa profilu Bieżąca wartość bazowa - Stosunek węglowodanów profilu %1$.2f ograniczone do %2$.2f Wartość »%1$s« jest poza dopuszczalną granicą - Wartość »%1$s« %2$.2f jest poza dopuszczalną granicą - Wartość bazy BOLUS %1$.2f U @@ -426,10 +403,6 @@ Doradca bolusa Masz wysoką glikemię. Zamiast jeść teraz, zaleca się poczekać na wyrównanie poziomu cukru. Czy chcesz wykonać teraz bolus korekcyjny i otrzymać przypomnienie, kiedy nadejdzie czas na posiłek? W tym przypadku żadne węglowodany nie zostaną zarejestrowane i należy ponownie użyć kalkulatora, po otrzymaniu przypomnienia o posiłku. - COB vs IOB - Zastosowano ograniczenie bolusa: %1$.2f U do %2$.2f U - Bolus zostanie jedynie odnotowany (nie będzie podany przez pompę) - Uruchom alarm kiedy będzie czas na jedzenie Nie wybrano żadnej akcji, zdarzenie nie będzie wprowadzone Brak niezbędnego do obliczeń bieżącego pomiaru glikemi! Nie ustawiono aktywnego profilu! @@ -559,7 +532,6 @@ Pon %1$.1f U - %1$.2f U Dawka: %1$.2f%% (%2$.2f U/h) Czas: %3$d min Dawka: %1$.2f U/h (%2$.2f%%) Czas: %3$d min Dawka: %1$.2f%% (%2$.2f U/h)
Czas: %3$d min
]]>
diff --git a/core/ui/src/androidMain/res/values-pt-rBR/strings.xml b/core/ui/src/androidMain/res/values-pt-rBR/strings.xml index f113f452b6be..a20c990c0e1c 100644 --- a/core/ui/src/androidMain/res/values-pt-rBR/strings.xml +++ b/core/ui/src/androidMain/res/values-pt-rBR/strings.xml @@ -43,13 +43,10 @@ DDT Total Iniciando aplicação de %1$.2f U AAPS iniciado - %1$+.2f U - %1$d g %1$.2f h Objetivos Por favor aguarde… Parar - Carbos Perfil inválido! SEM PERFIL DEFINIDO ]]> @@ -90,12 +87,9 @@ Os dados estão vindo de uma bomba diferente. Alterar o driver da bomba para redefinir o estado da bomba. GLIC Calibração - Disparar alarme em %1$d min Duração g - Bomba suspensa Não configurado - Loop suspenso req caindo rapidamente caindo @@ -138,11 +132,6 @@ As passwords não coincidem PINs não conferem - Valores das basais não definidos por horas: %1$s - Valor da basal alterado para o valor mínimo suportado: %1$s - Valor da basal alterado para o valor máximo suportado: %1$s - U/h - g/U Iniciar perfil %1$d%% para %2$d min @@ -157,7 +146,6 @@ Média - %1$d min Careportal Verificação BG @@ -200,7 +188,6 @@ Personalizado Loop NS - Gravar Ligação expirou @@ -213,7 +200,6 @@ %1$d g de carboidratos necessários em %2$d minutos Basal - Bólus Deteção de tempo @@ -223,7 +209,6 @@ BOLUS ESTENDIDO TBR SUPERBOLUS CARBOIDRATOS - CARBOIDRATOS ESTENDIDOS BASAL TEMPORÁRIO ALVO TEMPORÁRIO NOVO PERFIL @@ -306,20 +291,13 @@ LOOP ALTERADO LOOP REMOVIDO - Alvo de perfil de hipoglicemia - Alvo de perfil de hiperglicemia Valor menor do alvo temporário Valor maior do alvo temporário Valor do alvo temporário - Valor do perfil da DAI - Valor do perfil de sensibilidade Valor basal máximo do perfil Valor basal atual - Valor do perfil da taxa de carboidratos %1$.2f limitado a %2$.2f >>%1$s<< está fora dos limites estabelecidos - »%1$s« %2$.2f está fora dos limites estabelecidos - Valor basal Bolus de %1$.2f U @@ -354,10 +332,6 @@ Assistente de bolus Sua glicemia está alta. Em vez de comer agora, é recomendado esperar por uma glicemia melhor. Quer fazer um bolus de correção agora e ser lembrado de quando for hora de comer? Neste caso, nenhum carboidrato será registrado e você deverá usar o assistente novamente quando lembrarmos você. - CA vs IA - Restrição de bólus aplicada: %1$.2f U para %2$.2f U - Bolus será apenas registrado (não administrado pela bomba) - Disparar alarme quando for a hora de comer Nenhuma acção seleccionada, nada irá acontecer Nenhuma glicemia recente para base de cálculo! Nenhum perfil ativo definido! diff --git a/core/ui/src/androidMain/res/values-pt-rPT/strings.xml b/core/ui/src/androidMain/res/values-pt-rPT/strings.xml index 939999c9e9ed..569f175ae7ca 100644 --- a/core/ui/src/androidMain/res/values-pt-rPT/strings.xml +++ b/core/ui/src/androidMain/res/values-pt-rPT/strings.xml @@ -43,13 +43,10 @@ TID Total A ser administrado %1$.2f U AAPS iniciada - %1$+.2f U - %1$d g %1$.2f h Objectivos Por favor aguarde… Parar - Hidratos Perfil inválido! SEM PERFIL DEFINIDO ]]> @@ -90,12 +87,9 @@ Os dados vêm de uma bomba diferente. Altera o driver da bomba para redefinir o seu estado. GLIC Calibração - Executar alarme em %1$d min Duração g - Bomba suspensa Não configurado - Loop suspenso req a descer rapidamente a descer @@ -138,11 +132,6 @@ Palavras-passe não correspondem Os PINs não correspondem - Valores das basais não definidos por horas: %1$s - Valor da basal alterado para o valor mínimo suportado: %1$s - Valor da basal alterado para o valor máximo suportado: %1$s - U/h - g/U Iniciar perfil %1$d%% para %2$d min @@ -157,7 +146,6 @@ Média - %1$d min Careportal Verificar Glicose @@ -200,7 +188,6 @@ Personalizado Loop NS - Registo Ligação expirou @@ -213,7 +200,6 @@ %1$d g Hidratos Adicionais Necessários Dentro de %2$d Minutos Basal - Bólus Detecção de tempo @@ -223,7 +209,6 @@ BÓLUS PROLONGADO DBT SUPERBÓLUS HC - HC LENTOS BASAL TEMPORÁRIA ALVO TEMPORÁRIO NOVO PERFIL @@ -306,20 +291,13 @@ LOOP ALTERADO LOOP REMOVIDO - Valor mínimo alvo do perfil - Valor máximo alvo do perfil Valor inferior do alvo temporário Valor superior do alvo temporário Valor Alvo Temporário - Valor Perfil DIA - Valor Perfil Sensibilidade Valor Perfil Basal Máxima Valor Actual Basal - Valor Perfil Rácio Hidratos %1$.2f limitado a %2$.2f »%1$s« está fora dos limites máximos - »%1$s« %2$.2f está fora dos limites permitidos - Valor da Basal BOLUS %1$.2f U @@ -344,8 +322,6 @@ Alarme Urgente INFO - HCA vs IA - Executar alarme quando for tempo de comer Nenhuma ação seleccionada, nada irá acontecer Adolescente @@ -437,7 +413,6 @@ Seg %1$.1f U - %1$.2f U diff --git a/core/ui/src/androidMain/res/values-ro-rRO/strings.xml b/core/ui/src/androidMain/res/values-ro-rRO/strings.xml index a6a1d7213a8b..b402e450d865 100644 --- a/core/ui/src/androidMain/res/values-ro-rRO/strings.xml +++ b/core/ui/src/androidMain/res/values-ro-rRO/strings.xml @@ -38,8 +38,6 @@ Tip eveniment mg/dL mmol/L - mg/dL/U - mmol/l/U %1$d mg/dL %1$.1f mmol/l Salvați @@ -106,18 +104,14 @@ Se vor administra %1$.2fU Bolus administrat, dar carbohidrații nu au putut fi salvați. Vă rugăm să introduceți carbohidrații din nou. AAPS pornit - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Obiective Așteptați… Stop STOP APĂSAT - Carbohidrați Profil invalid! NICIUN PROFIL SETAT Nu se poate schimba profilul: nicio insulină nu este în uz și nu a fost selectată niciuna. @@ -173,15 +167,12 @@ Glicemie Calibrare CGM - Rulați alarma în %1$d minute Bolusarea a raportat o eroare. Verificați manual insulina administrată și cantitatea de carbohidrați Reamintiți de bolus atunci când glicemia revine Durată g - Pompă suspendată Pompa în funcțiune Nu este configurat - Buclă suspendată Bucla suspendată din cauza orei de vară necesar în scădere rapidă @@ -231,7 +222,6 @@ Buclă închisă Buclă deschisă Suspendare la glicemie scăzută - Pompă deconectată Pompă suspendată Suspendați pompa Reluați pompa @@ -267,11 +257,6 @@ Parolele nu corespund PIN-urile nu se potrivesc - Valori bazale nesincronizate cu ora: %1$s - Valoarea bazalei a fost înlocuită cu valoarea minimă posibilă: %1$s - Valoarea bazalei a fost înlocuită cu valoarea maximă posibilă: %1$s - U/h - g/U Utilizați profilul %1$d%% pentru %2$d min @@ -309,7 +294,6 @@ 30z - %1$d min Careportal Verificare glicemie @@ -493,7 +477,6 @@ Reveniți la valorile implicite Buclă NS - Înregistrare Piept dreapta Piept stânga Partea superioară laterală a brațului drept @@ -543,7 +526,6 @@ %1$d g carbohidrați suplimentari necesari în %2$d minute Bazală - Bolus DZT (Doza zilnică totală) Doza zilnică totală @@ -555,7 +537,6 @@ BOLUS EXTINS RBT SUPERBOLUS CARBOHIDRAȚI - CARBOHIDRAȚI EXTINȘI BAZALĂ TEMPORARĂ ȚINTĂ TEMPORARĂ PROFIL NOU @@ -656,22 +637,15 @@ SCENARIU DEZACTIVAT CONFIGURAȚIA PENTRU TELECOMANDĂ MODIFICATĂ - Profil ținta joasă - Profil țintă ridicată Limita inferioară a țintei temporare Limita superioară a țintei temporare Valoare țintă temporară - Valoare profil DIA Valoarea DIA a insulinei Vârful de acțiune al insulinei - Valoare sensibilitate profil Valoare bazală maximă a profilului Valoare bazală curentă - Valoarea raportului carbohidrați din profil %1$.2f limitat la %2$.2f »%1$s« este in afara limitelor stabilite - »%1$s« %2$.2f este in afara limitelor stabilite - Valoare rata bazală Niciun profil selectat Lipsește numele profilului @@ -728,18 +702,12 @@ Consilier bolus Ai glicemia crescută. În loc să mănânci acum, este recomandat să aştepți o glicemie mai bună. Vrei să faci un bolus de corecție acum și să îți reamintesc când este timpul să mănânci? În acest caz, niciun carbohidrat nu va fi înregistrat și trebuie să utilizezi din nou calculatorul de vbolus când îți voi reaminti. - COB vs IOB - !!!!! S-a detectat o absorbție lentă de carbohidrați: %1$d%% din timp. Verificați de două ori calculul. COB poate fi supraestimat, astfel încât mai multă insulină poate fi administrată !!!!! - Este aplicată limitarea bolusului %1$.2f U la %2$.2f U - Bolusul doar va fi înregistrat (nu va fi administrat de pompă) - Executați alarma când este timpul să mâncați Glicemie Corecție Subtotal Pâine, patiserie sărată, iaurt, fructe (măr, banană, pară), cartofi, orez, hrișcă, ovăz, alimente cu carbohidrați lenți și puțini, legume Găluște, paste, bucătărie asiatică bogată în carbohidrați și grăsimi, patiserie dulce și cu drojdie, cereale, terci de cereale, miere, gem Lasagna, pizza, hamburgeri, cartofi prăjiți, cartofi copți, chipsuri și gustări similare - carbohidrați extinși %1$dg / %2$dh (+%3$dmin) Total Timp carbohidrați Acum @@ -987,7 +955,6 @@ Lun %1$.1f U - %1$.2f U %1$+.2f U Rată: %1$.2f%% (%2$.2f U/h) Durată: %3$d min Rată: %1$.2f U/h (%2$.2f%%) Durată: %3$d min diff --git a/core/ui/src/androidMain/res/values-ru-rRU/strings.xml b/core/ui/src/androidMain/res/values-ru-rRU/strings.xml index 625f72e95d74..4544af3934e9 100644 --- a/core/ui/src/androidMain/res/values-ru-rRU/strings.xml +++ b/core/ui/src/androidMain/res/values-ru-rRU/strings.xml @@ -44,13 +44,10 @@ Старые данные Будет подано %1$.2f ед инс AAPS запущен - %1$+.2f ед - %1$d гр %1$.2f ч Цели Подождите… Стоп - Углеводы Недопустимый профиль! ПРОФИЛЬ НЕ ЗАДАН @@ -94,14 +91,11 @@ Данные поступают с другой помпы. Измените драйвер помпы, чтобы сбросить ее состояние. ГК калибровка - Напомнить через %1$d мин Возможная ошибка подачи болюса. Проверьте количество реально поданного инсулина и углеводов Длительность действия грамм - Работа помпы остановлена Помпа работает Не настроено - ЗЦ остановлен Помпа остановлена при переходе времени надо быстрое падение @@ -134,7 +128,6 @@ Замкнутый цикл Открытый цикл Приостановка помпы на низкой ГК - Помпа отключена Работа помпы приостановлена Режим возвращен к предыдущему состоянию Время действия инсулина DIA @@ -160,11 +153,6 @@ Пароли не совпадают PIN-коды не совпадают - Базальные значения не выровнены по часам: %1$s - Значение базала заменено минимальной поддерживаемой величиной: %1$s - Значение базала заменено максимальной поддерживаемой величиной: %1$s - ед/ч - г/ед Начать профиль %1$d%% на %2$d мин @@ -179,7 +167,6 @@ Средний - %1$d мин. Портал терапии Проверка ГК @@ -227,7 +214,6 @@ Пользовательский Замкнутый цикл NS - Запись Правая сторона груди Левая сторона груди Верхняя внешняя часть правого плеча @@ -273,7 +259,6 @@ Необходимо дополнительно %1$d г углеводов в течение %2$d минут базал - Болюс Определение времени @@ -283,7 +268,6 @@ ПРОЛОНГИРОВАННЫЙ БОЛЮС СУПЕРБОЛЮС ВБС/TBR УГЛЕВОДЫ - ПРОЛОНГИРОВАННЫЕ УГЛЕВОДЫ ВРЕМЕННЫЙ БАЗАЛ ВРЕМ ЦЕЛЬ НОВЫЙ ПРОФИЛЬ @@ -375,20 +359,13 @@ РАБОЧИЙ РЕЖИМ УДАЛЕН РАБОЧИЙ РЕЖИМ ОБНОВЛЕН - Нижнее целевое значение профиля - Верхнее целевое значение профиля Нижнее значение временного целевого уровня Верхнее значение временного целевого уровня Временное целевое значение - Значение длительности действия инсулина DIA в профиле - Значение чувствительности в профиле Максимальное значение базала в профиле Текущее значение базала - Значение Углеводного коэффициента IC в профиле %1$.2f ограничено до %2$.2f »%1$s« за пределами жестких ограничений - »%1$s« %2$.2f за пределами жестких ограничений - Величина базала БОЛЮС %1$.2f ЕД @@ -426,10 +403,6 @@ Помощник болюса У вас высокая гликемия. Лучше подождать, чем есть сейчас. Хотите сделать болюс на коррекцию и установить напоминание о приеме пищи? В этом случае углеводы не будут записаны и после напоминания надо будет снова воспользоваться помощником. - угл COB к инс IOB - Применено ограничение болюса: %1$.2f ед. до %2$.2f ед. - Болюс будет только записан (без подачи помпой) - Напомнить о еде Действие не выбрано, ничего не произойдет Нет данных ГК для основы расчета! Активный профиль не установлен! @@ -559,7 +532,6 @@ Пн %1$.1fед - %1$.2f ед Скорость: %1$.2f%% (%2$.2f U/h) Продолжительность: %3$d мин Скорость: %1$.2fед/ч (%2$.2f%%) Продолжительность %3$d мин Скорость:%1$.2f %% (%2$.2f ед/ч)
Продолжительность %3$d мин
]]>
diff --git a/core/ui/src/androidMain/res/values-sk-rSK/strings.xml b/core/ui/src/androidMain/res/values-sk-rSK/strings.xml index ddec624b9a43..fcdb79c5201d 100644 --- a/core/ui/src/androidMain/res/values-sk-rSK/strings.xml +++ b/core/ui/src/androidMain/res/values-sk-rSK/strings.xml @@ -38,8 +38,6 @@ Typ udalosti mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/l Uložiť @@ -105,18 +103,14 @@ Staré dáta Podávanie %1$.2f U inzulínu AAPS spustený - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Ciele Čakajte prosím... Stop STLAČENÝ STOP - Sacharidy Neplatný profil! NENASTAVENÝ ŽIADNY PROFIL ]]> @@ -166,15 +160,12 @@ Glykémia Kalibrácia CGM - Spustiť výstrahu za %1$d min Bolus zaznamenal chybu. Manuálne skontrolujte skutočne podané množstvo inzulínu a sacharidov Upozornenie na bolus keď sa obnoví glykémia Trvanie g - Pumpa pozastavená Pumpa beží Nenakonfigurované - Uzavretý okruh pozastavený Uzavretý okruh pozastavený kvôli zmene času pož. rýchly pokles @@ -224,7 +215,6 @@ Uzavretý okruh Otvorený okruh Pozastavenie pri nízkej glykémii (LGS) - Pumpa odpojená Pumpa pozastavená Pozastaviť pumpu Obnoviť pumpu @@ -260,11 +250,6 @@ Heslá sa nezhodujú PIN kódy sa nezhodujú - Bazálne hodnoty nie sú zarovnané na celé hodiny: %1$s - Hodnota bazálu nahradená minimálnou možnou: %1$s - Hodnota bazálu nahradená maximálnou možnou: %1$s - U/h - g/U Spustiť profil %1$d%% na %2$d min @@ -302,7 +287,6 @@ 30d - %1$d min. Starostlivosť Kontrola glykémie @@ -486,7 +470,6 @@ Obnoviť predvolené Uzavretý okruh NS - Záznam Hruď vpravo Hruď vľavo Horná, vonkajšia strana pravej ruky @@ -536,7 +519,6 @@ Požadovaných dodatočných %1$d g sacharidov v priebehu %2$d minút Bazál - Bolus CDD Celková denná dávka @@ -548,7 +530,6 @@ PREDĹŽENÝ BOLUS SUPERBOLUS TBR SACHARIDY - ROZLOŽENÉ SACHARIDY DOČASNÝ BAZÁL DOČASNÝ CIEĽ NOVÝ PROFIL @@ -649,22 +630,15 @@ SCÉNA DEAKTIVOVANÁ VZDIALENÁ KONFIGURÁCIA ZMENENÁ - Dolný cieľ profilu - Horný cieľ profilu Dolná hodnota dočasného cieľa Horní hodnota dočasného cíle Hodnota dočasného cieľa - Profilová hodnota DIA DIA hodnota inzulínu Inzulínová hodnota vrcholu - Profilová hodnota citlivosti Profilová maximálna hodnota bazálu Aktuálna hodnota bazálu - Profilový inzulino-sacharidový pomer %1$.2f obmedzené na %2$.2f »%1$s« je mimo pevne nastavené limity - »%1$s« %2$.2f je mimo pevne nastavených limitov - Hodnota bazálu Nebol vybraný žiadny profil Chýba názov profilu @@ -721,18 +695,12 @@ Bolusový poradca Máte vysokú glykémiu. Namiesto jedla doporučujeme počkať na lepšiu glykémiu a pripomenúť, keď bude čas na jedlo. Prajete si poslať korekčný bolus a pripomenúť, keď bude čas na jedlo? V tomto prípade nebudú zapísané žiadne sacharidy, a neskôr musíte opäť spustiť kalkulačku, akonáhle vám to pripomenieme. - COB vs. IOB - !!!!! Detekovaná pomalá absorbcia sacharidov: %1$d%% času. Radšej dvakrát skontrolujte kalkuláciu. COB môže byť úplne iné, môže byť podaného viac inzulínu!!!!! - Použité obmedzenie bolusu: %1$.2f U na %2$.2f U - Bolus bude iba zaznamenaný (nie pumpou vydaný) - Spustiť výstrahu, keď je čas na jedlo Glykémia Korekcia Medzisúčet Chlieb, slané pečivo, jogurt, ovocie (jablko, banán, hruška), zemiaky, ryža, pohánka, ovsenné vločky, pomalé a nízko-sacharidové jedlá, zelenina Halušky, cestoviny, ázijská kuchyňa bohatá na sacharidy a tuky, sladké a kysnuté pečivo, cereálie, obilná kaša, med, džem Lasagne, pizza, hamburgery, hranolky, pečené zemiaky, chipsy a podobné snacky - eSacharidy %1$dg / %2$dh (+%3$dmin) Spolu Čas sacharidov Teraz @@ -977,7 +945,6 @@ Pon %1$.1f U - %1$.2f U %1$+.2f U Hodnota: %1$.2f%% (%2$.2f U/h) Trvanie: %3$d min Hodnota: %1$.2f U/h (%2$.2f%%) Trvanie: %3$d min diff --git a/core/ui/src/androidMain/res/values-sr-rCS/strings.xml b/core/ui/src/androidMain/res/values-sr-rCS/strings.xml index ccd67e6f99cd..0a38469c8be0 100644 --- a/core/ui/src/androidMain/res/values-sr-rCS/strings.xml +++ b/core/ui/src/androidMain/res/values-sr-rCS/strings.xml @@ -12,7 +12,6 @@ Pumpa IOB: Isporučujem %1$.2f U - Uglj. hidrati Basal Izađi IOB @@ -45,7 +44,6 @@ Basal - Bolus diff --git a/core/ui/src/androidMain/res/values-sv-rSE/strings.xml b/core/ui/src/androidMain/res/values-sv-rSE/strings.xml index 4a0bb67b81af..d1cc553a4a2e 100644 --- a/core/ui/src/androidMain/res/values-sv-rSE/strings.xml +++ b/core/ui/src/androidMain/res/values-sv-rSE/strings.xml @@ -43,13 +43,10 @@ TDD totalt Kommer att leverera %1$.2f enheter AAPS startad - %1$+.2f U - %1$dg %1$.2f h Mål Var god vänta… Stopp - Kolhydrater Ogiltig profil! INGEN PROFIL VALD ]]> @@ -90,12 +87,9 @@ Data kommer från en annan pump. Byt pumpdrivrutin för att återställa. BG Kalibrering - Larma om %1$d min Duration g - Pump pausad Inte konfigurerad - Loop pausad beh sjunker snabbt sjunker @@ -138,11 +132,6 @@ Lösenorden stämmer inte överens PIN-koderna överensstämmer inte - Profilens basaler är inte satta på hel timme: %1$s - Basalvärdet ersatt med det lägsta tillåtna: %1$s - Basalvärdet ersatt med det högsta tillåtna: %1$s - U/h - g/U Byt till profil %1$d%% i %2$d min @@ -157,7 +146,6 @@ Medel - %1$d min Careportal BG-kontroll @@ -200,7 +188,6 @@ Anpassad Loop NS - Post Anslutningen tog för lång tid @@ -213,7 +200,6 @@ %1$d g kolhydrater behövs inom %2$d minuter Basal - Bolus Tidsdetektering @@ -223,7 +209,6 @@ FÖRLÄNGD BOLUS SUPERBOLUS TEMPBASAL KH - FÖRLÄNGDA KH TEMP BASAL TEMP MÅL NY PROFIL @@ -306,20 +291,13 @@ LOOP ÄNDRAD LOOP BORTTAGEN - Nedre målvärde för profilen - Övre målvärde för profilen Nedre värde för temp mål Övre värde för temp mål Temp målvärde - Profilens DIA - Profilens insulinkänslighetsvärde Maximal basaldos för profil Nuvarande basaldos - Profilens KH-kvot %1$.2f begränsat till %2$.2f »%1$s« är utanför de hårda gränserna - »%1$s« %2$.2f är utanför hårda gränser - Basaldos BOLUS %1$.2f U @@ -354,10 +332,6 @@ Bolusguide Ditt blodsocker är högt. Istället för att äta är det rekommenderat att vänta tills det sjunker. Vill du göra en korrigeringsbolus nu och få en påminnelse när det är dags att äta? I det här fallet kommer inga kolhydrater att registreras nu, utan du måste ange måltiden på nytt i kalkylatorn. - COB kontra IOB - Bolusbegränsning tillämpad: %1$.2f U till %2$.2f U - Bolus kommer bara att loggas (inte levereras av pumpen) - Larma när det är dags att äta Ingen åtgärd vald. Inget ändras. Inget nytt BG-värde att basera beräkning på! Ingen aktiv profil vald! @@ -461,7 +435,6 @@ Rensade poster - %1$.2fU diff --git a/core/ui/src/androidMain/res/values-tr-rTR/strings.xml b/core/ui/src/androidMain/res/values-tr-rTR/strings.xml index b6db66cdb8f4..a860ecfb37e4 100644 --- a/core/ui/src/androidMain/res/values-tr-rTR/strings.xml +++ b/core/ui/src/androidMain/res/values-tr-rTR/strings.xml @@ -44,13 +44,10 @@ Eski veri %1$.2f Ü gönderilecek AAPS başladı - %1$+.2f Ü - %1$d g %1$.2f s Görevler Lütfen bekleyin… Dur - Karbonhidrat Geçersiz profil! PROFİL AYARLANMADI ]]> @@ -93,14 +90,11 @@ Veriler farklı pompadan geliyor. Pompa durumunu sıfırlamak için pompa sürücüsünü değiştirin. Kalibrasyon - Alarmı %1$d dakika içinde çalıştır Bolus bir hata bildirdi. Gerçek iletilen insülin ve karbonhidrat miktarını manuel olarak kontrol edin Süre gr - Pompa Durduruldu Pompa çalışıyor Yapılandırılmadı - Döngü duraklatıldı iht. hızla düşüyor düşüyor @@ -130,7 +124,6 @@ Kapalı Döngü Açık Döngü Düşük Glikoz Duraklatma (LGS) - Pompa bağlantısı kesildi Pompa Durduruldu Mod Döndü İES @@ -156,11 +149,6 @@ Şifreler eşleşmiyor PIN\'ler eşleşmiyor - Bazal değerler saatlerle uyumlu değil: %1$s - Desteklenen minimum değerle değiştirilen bazal değer: %1$s - Bazal değeri maksimum desteklenen değerle değiştirilir: %1$s - Ü/s - g/Ü %2$d dakika için %1$d%% profilini başlat @@ -175,7 +163,6 @@ Ortalama - %1$d dak Bakım Portalı KŞ Kontrol @@ -221,7 +208,6 @@ Özel Döngü NS - Kayıt Bağlantı zaman aşımına uğradı @@ -234,7 +220,6 @@ %2$d dakika içinde %1$d g ek karbonhidrat gerekiyor Bazal - Bolus Zaman algılama @@ -244,7 +229,6 @@ YAYMA BOLUS SÜPERBOLUS GBO KARBLR - YAYMA KARBONHİDRAT GEÇİCİ BAZAL GEÇİCİ HEDEF YENİ PROFİL @@ -330,20 +314,13 @@ DÖNGÜ DEĞİŞTİ DÖNGÜ KALDIRILDI - Düşük hedef profili - Yüksek hedef profili Geçici hedef alt değeri Geçici hedef üst değeri Geçici hedef değeri - Profil DIA değeri - Profil duyarlılık değeri Azami profil bazal değeri Mevcut bazal değer - Profil karbonhidrat oranı değeri %1$.2f, %2$.2f ile sınırlıdır »%1$s« sınırların dışında - »%1$s« %2$.2f sınırların dışında - Bazal değer İDF değerinde hata @@ -382,10 +359,6 @@ Bolus danışmanı Yüksek glisemiksiniz. Şimdi yemek yemek yerine daha iyi glisemi beklemeniz önerilir. Şimdi bir düzeltme bolusu yapmak ve yemek zamanı geldiğinde size hatırlatmak ister misiniz? Bu durumda karbonhidrat kaydı yapılmaz ve size hatırlattığımızda sihirbazı tekrar kullanmanız gerekir. - AKRB vs AİNS - Bolus kısıtlaması uygulandı: %1$.2f Ü ile %2$.2f Ü - Bolus yalnızca kaydedilecektir (pompa ile iletilmez) - Yemek zamanı alarmı çalıştır Seçili eylem yok, hiçbir şey olmayacak Hesaplamayı temel alacak yeni bir KŞ yok! Etkin profil ayarlanmadı! @@ -509,7 +482,6 @@ Pts %1$.1f Ü - %1$.2f Ü Oran: %1$.2f%% (%2$.2f Ü/s) Süre: %3$d dk Oran: %1$.2f Ü/s (%2$.2f%%) Süre: %3$d dk Oran: %1$.2f%% (%2$.2f Ü/s)
Süre: %3$d dk
]]>
diff --git a/core/ui/src/androidMain/res/values-uk-rUA/strings.xml b/core/ui/src/androidMain/res/values-uk-rUA/strings.xml index a6b3034c74fd..b5e7b2b7c819 100644 --- a/core/ui/src/androidMain/res/values-uk-rUA/strings.xml +++ b/core/ui/src/androidMain/res/values-uk-rUA/strings.xml @@ -51,7 +51,6 @@ - %1$.2f од diff --git a/core/ui/src/androidMain/res/values-vi-rVN/strings.xml b/core/ui/src/androidMain/res/values-vi-rVN/strings.xml index a7e1e07d1c1d..25b4477d1e91 100644 --- a/core/ui/src/androidMain/res/values-vi-rVN/strings.xml +++ b/core/ui/src/androidMain/res/values-vi-rVN/strings.xml @@ -38,8 +38,6 @@ Loại sự kiện mg/dl mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Lưu @@ -106,18 +104,14 @@ Đang tiêm %1$.2f U Insulin Đã tiêm bolus, nhưng không thể lưu lượng carb. Vui lòng nhập lại lượng carb. AAPS đã khởi động - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d phút - %1$s: %2$s %1$s %2$s Mục tiêu học tập Vui lòng chờ… Dừng lại Đã nhấn DỪNG - Carbs Cấu hình không hợp lệ! KHÔNG CÓ CẤU HÌNH Không thể chuyển cấu hình điều trị: không có loại insulin nào đang được sử dụng và cũng chưa có loại nào được chọn. @@ -173,15 +167,12 @@ BG Hiệu chuẩn CGM - Báo động sau %1$d phút Bolus gặp lỗi. Hãy kiểm tra thủ công lượng carb và insulin đã bơm Nhắc tiêm bolus khi đường huyết hồi phục Thời lượng g - Bơm đã tạm dừng Bơm đang chạy Chưa được thiết lập - Vòng lặp đã tạm dừng Vòng lặp bị tạm dừng do thay đổi múi giờ req giảm nhanh @@ -231,7 +222,6 @@ Vòng lặp kín Vòng lặp mở Tạm dừng khi BG Thấp - Bơm đã ngắt kết nối Bơm đã tạm ngưng Tạm dừng bơm Tiếp tục bơm @@ -267,11 +257,6 @@ Mật khẩu không khớp Mã PIN không khớp - Giá trị liều nền không khớp với giờ: %1$s - Giá trị liều nền đã được thay bằng giá trị tối thiểu được hỗ trợ: %1$s - Giá trị liều nền đã được thay bằng giá trị tối đa được hỗ trợ: %1$s - U/h - g/U Bắt đầu cấu hình %1$d%% trong %2$d phút @@ -309,7 +294,6 @@ 30d - %1$d phút Thông tin điều trị Kiểm tra BG @@ -493,7 +477,6 @@ Khôi phục về mặc định Vòng lặp NS - Record Ngực phải Ngực trái Phần ngoài cánh tay phải phía trên @@ -543,7 +526,6 @@ Cần thêm %1$d g carb trong vòng %2$d phút Basal - Bolus TDD Tổng liều hàng ngày @@ -555,7 +537,6 @@ LIỀU BOLUS KÉO DÀI SUPERBOLUS TBR CARBS - CARBS MỞ RỘNG TEMP BASAL TEMP TARGET CẤU HÌNH MỚI @@ -656,22 +637,15 @@ ĐÃ TẮT NGỮ CẢNH CẤU HÌNH TỪ XA ĐÃ THAY ĐỔI - Cấu hình mục tiêu thấp - Cấu hình mục tiêu cao Giá trị thấp của mục tiêu tạm thời Giá trị cao của mục tiêu tạm thời Giá trị mục tiêu tạm thời - Cấu hình giá trị DIA Thời gian tác dụng của insulin Thời điểm đạt đỉnh của insulin - Cấu hình giá trị độ nhạy Cấu hình liều nền tối đa Giá trị liều nền hiện tại - Cấu hình tỷ lệ Carb %1$.2f bị giới hạn ở %2$.2f »%1$s« nằm ngoài giới hạn an toàn - »%1$s« %2$.2f nằm ngoài giới hạn an toàn - Giá trị liều liều nền Chưa chọn hồ sơ Thiếu tên cấu hình @@ -728,18 +702,12 @@ Gợi ý bolus Bạn đang có đường huyết cao. Thay vì ăn ngay, nên chờ đến khi đường huyết ổn định hơn. Bạn có muốn thực hiện bolus chỉnh sửa ngay và được nhắc khi đến giờ ăn? Trong trường hợp này, không có carbs nào được ghi nhận và bạn sẽ phải sử dụng wizard một lần nữa khi được nhắc. - COB với IOB - !!!!! Phát hiện hấp thu carb chậm trong %1$d%% thời gian. Hãy kiểm tra lại phép tính của bạn. COB có thể bị ước tính cao, do đó có thể dẫn đến việc cung cấp nhiều insulin hơn !!!!! - Giới hạn bolus đã áp dụng: %1$.2f U đến %2$.2f U - Liều Bolus chỉ được ghi lại (không tiêm qua Bơm) - Chạy báo thức khi đến giờ ăn BG Hiệu chỉnh Tổng phụ Bánh mì, bánh mặn, sữa chua, trái cây (táo, chuối, lê), khoai tây, cơm, kiều mạch, bột yến mạch, thực phẩm giàu carb chậm và thấp, rau củ Bánh bao, mì ống, ẩm thực châu Á giàu carb và chất béo, bánh ngọt và bánh men, ngũ cốc, cháo từ ngũ cốc, mật ong, mứt Lasagna, pizza, hamburger, khoai tây chiên, khoai tây nướng, snack như khoai tây lát chiên và các món tương tự - eCarbs %1$dg / %2$dh (+%3$dmin) Tổng cộng Thời gian ăn Bây giờ @@ -981,7 +949,6 @@ Mon %1$.1f U - %1$.2f U %1$+.2f U Tốc độ: %1$.2f%% (%2$.2f U/h) Thời gian: %3$d phút Tốc độ: %1$.2f U/h (%2$.2f%%) Thời gian: %3$d min diff --git a/core/ui/src/androidMain/res/values-zh-rCN/strings.xml b/core/ui/src/androidMain/res/values-zh-rCN/strings.xml index 7c5e52be9185..9a1aa775bb92 100644 --- a/core/ui/src/androidMain/res/values-zh-rCN/strings.xml +++ b/core/ui/src/androidMain/res/values-zh-rCN/strings.xml @@ -38,8 +38,6 @@ 事件类型 mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L 保存 @@ -105,18 +103,14 @@ 旧的数据 将要输注 %1$.2f U AAPS 已启动 - %1$+.2f U - %1$d 克 %1$d+%2$d g %1$.2f 小时 %1$d min - %1$s: %2$s %1$s %2$s 目标 请稍等... 停止 已按下停止 - 碳水化合物 无效的配置文件 没有配置文件 无法切换配置文件:没有正在使用的胰岛素,也未选择任何胰岛素。 @@ -172,15 +166,12 @@ 血糖 校准 CGM - 在 %1$d 分钟内运行提醒 大剂量输注报告了一个错误。请手动检查实际输送的胰岛素和碳水化合物量 当血糖恢复时提醒推注 持续时间 - 泵暂停了 泵运行中 未配置 - 闭环暂停了 循环因夏令时DST而暂停 迅速下降 @@ -230,7 +221,6 @@ 闭环 开环 低血糖维持模式 - 泵已断开 泵已暂停 暂停泵 恢复泵 @@ -266,11 +256,6 @@ 密码不匹配 PIN 码不匹配。 - 基础率值与小时数不一致:%1$s - 基础率已设为支持的最小值:%1$s - 基础率值被泵支持的最大值:%1$s 替换了 - U/h - 克/U 启动配置文件 %1$d%%在 %2$d 分钟 @@ -308,7 +293,6 @@ 30天 - %1$d 分钟 护理记录 指血检查 @@ -492,7 +476,6 @@ 恢复为默认值 闭环 NS - 记录 右胸 左胸 右上臂外侧 @@ -542,7 +525,6 @@ %2$d 分钟内需要额外摄入 %1$d 克碳水化合物 基础率 - 大剂量 TDD 每日总剂量 @@ -554,7 +536,6 @@ 扩展大剂量 超级大剂量TBR 碳水 - 扩展碳水 临时基础率 临时目标 新配置文件 @@ -655,22 +636,15 @@ 情景已停用 远程配置已更改 - 个人配置低目标 - 个人配置高目标 临时目标值下限值 临时目标值上限值 临时目标值 - 配置文件DIA值 胰岛素作用时间 胰岛素峰值时间 - 配置文件敏感系数值 配置文件最大基础率值 当前基础率值 - 配置文件碳水系数值 %1$.2f 超过 %2$.2f的限制 »%1$s« 超出了硬限制 - »%1$s« %2$.2f 超出了硬限制 - 基础率值 未选择配置文件 缺少配置文件名称 @@ -727,18 +701,12 @@ 大剂量向导 您当前血糖偏高。建议暂时不要进食,等待血糖降至正常。是否现在进行校正推注,并在适宜进食时提醒您?此操作将不会记录碳水化合物,收到提醒后您必须再次使用向导。 - 活性碳水vs活性胰岛素 - !!!!!检测到碳水化合物吸收缓慢:%1$d%%的时间。仔细检查你的计算。COB可能被高估,因此可能注射过多的胰岛素!!!!! - 已应用推注限制: %1$.2f U 到 %2$.2f U - 仅记录大剂量数值(泵不会输注) - 在应当吃饭时提醒 血糖 修正 小计 面包、咸味糕点、酸奶、水果(苹果、香蕉、梨)、土豆、大米、荞麦、燕麦片、低碳水化合物食物、蔬菜 饺子、意大利面、富含碳水化合物和脂肪的亚洲美食、甜食和酵母糕点、谷物、谷物粥、蜂蜜、果酱 烤宽面条、披萨、汉堡、薯条、烤土豆、薯片和类似小吃 - eCarbs %1$dg / %2$dh (+%3$dmin) 总计 碳水时间 现在 @@ -980,7 +948,6 @@ 星期一 %1$.1f U - %1$.2f U %1$+.2f U 速率:%1$.2f%% (%2$.2f U/h) 持续时间:%3$d 分钟 速率:%1$.2f U/h (%2$.2f%%) 持续时间:%3$d 分钟 diff --git a/core/ui/src/androidMain/res/values-zh-rTW/strings.xml b/core/ui/src/androidMain/res/values-zh-rTW/strings.xml index da3ed77e5a82..274d11d30f0e 100644 --- a/core/ui/src/androidMain/res/values-zh-rTW/strings.xml +++ b/core/ui/src/androidMain/res/values-zh-rTW/strings.xml @@ -38,8 +38,6 @@ 事件類型 mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L 儲存 @@ -106,18 +104,14 @@ 將注射 %1$.2f U 大劑量已注射 ,但無法儲存碳水。請重新輸入碳水。 AAPS 已啟動 - %1$+.2f U - %1$d g %1$d+%2$d 克 %1$.2f 小時 %1$d 分鐘 - %1$s: %2$s %1$s %2$s 目標 請稍候… 停止 已按下停止鍵 - 碳水化合物 無效的設定檔! 未設置設定檔 無法切換設定檔:目前沒有使用中的胰島素,且也未選取任何胰島素。 @@ -173,15 +167,12 @@ 血糖 校正 CGM - 在 %1$d 分鐘內運行警報 注射錯誤報告。請檢查實際注射的胰島素和碳水化合物量 當血糖恢復時提醒注射 持續時間 g - 幫浦已暫停 幫浦運行中 未配置 - 循環已暫停 因夏令時間切換,循環已暫停 需要 快速下降 @@ -231,7 +222,6 @@ 閉環 開環 低血糖暫停(LGS) - 幫浦已中斷連線 幫浦已暫停 暫停幫浦 恢復幫浦 @@ -267,11 +257,6 @@ 密碼不一致 PIN 碼不一致 - 基礎率值未對齊小時:%1$s - 基礎率值已被最低支援值取代:%1$s - 基礎率值已被最高支援值取代:%1$s - U/h - g/U 開始設定檔 %1$d%% 持續 %2$d 分鐘 @@ -309,7 +294,6 @@ 30 天 - %1$d 分鐘 照護入口 血糖檢查 @@ -495,7 +479,6 @@ 還原為預設值 循環 NS - 紀錄 右胸 左胸 右上臂外側 @@ -545,7 +528,6 @@ %1$d g 額外碳水化合物需要在 %2$d 分鐘內 基礎率 - 注射 TDD 每日總劑量 @@ -557,7 +539,6 @@ 延長注射 超級注射臨時基礎率 碳水化合物 - 延長碳水化合物 臨時基礎率 臨時目標 新設定檔 @@ -658,22 +639,15 @@ 場景已停用 遠端設定已變更 - 設定檔低目標 - 設定檔高目標 臨時目標低值 臨時目標高值 臨時目標值 - 設定檔 DIA 值 胰島素作用時間數值 胰島素峰值時間數值 - 設定檔敏感度值 最大設定檔基礎率值 目前基礎率值 - 設定檔碳水化合物比率值 %1$.2f 限制為 %2$.2f »%1$s« 超出硬限制範圍 - »%1$s« %2$.2f 超出硬限制範圍 - 基礎率值 未選擇設定檔 缺少設定檔名稱 @@ -730,18 +704,12 @@ 注射顧問 您的血糖過高。建議不要立即用餐,等待血糖恢復正常。您想現在進行修正注射,並提醒您何時用餐嗎?這種情況下不會紀錄碳水化合物,當我們提醒您時,您必須再次使用嚮導。 - COB 對 IOB - !!!!! 偵測到碳水吸收偏慢:%1$d%% 的時間。請再次檢查你的計算。活性碳水化合物可能被高估,因此可能會注射過多胰島素 !!!!! - 注射限制已套用:%1$.2f U 至 %2$.2f U - 僅紀錄注射(不由幫浦傳送) - 當到達用餐時間時提醒我 血糖 修正 小計 麵包、鹹點、優格、水果(蘋果、香蕉、梨)、馬鈴薯、白飯、蕎麥、燕麥、慢速與低碳水食物、蔬菜 水餃、義大利麵、富含碳水與脂肪的亞洲料理、甜點與酵母糕點、穀片、雜糧粥、蜂蜜、果醬 千層麵、披薩、漢堡、薯條、烤馬鈴薯、洋芋片及類似的零食 - eCarbs %1$dg / %2$dh (+%3$dmin) 總計 碳水時間 現在 @@ -983,7 +951,6 @@ 週一 %1$.1f U - %1$.2f U %1$+.2f U 速率:%1$.2f%%(%2$.2f U/h)持續時間:%3$d 分鐘 速率:%1$.2f U/h(%2$.2f%%)持續時間:%3$d 分鐘 diff --git a/core/ui/src/androidMain/res/values/strings.xml b/core/ui/src/androidMain/res/values/strings.xml index 109da3f8a4e5..1e3efca8136e 100644 --- a/core/ui/src/androidMain/res/values/strings.xml +++ b/core/ui/src/androidMain/res/values/strings.xml @@ -38,8 +38,6 @@ Event type mg/dL mmol/L - mg/dL/U - mmol/L/U %1$d mg/dL %1$.1f mmol/L Save @@ -105,18 +103,14 @@ Going to deliver %1$.2f U Bolus delivered, but the carbs could not be saved. Please enter the carbs again. AAPS started - %1$+.2f U - %1$d g %1$d+%2$d g %1$.2f h %1$d min - %1$s: %2$s %1$s %2$s Objectives Please wait… Stop STOP PRESSED - Carbs Invalid profile! NO PROFILE SET Cannot switch profile: no insulin is in use, and none was selected. @@ -172,15 +166,12 @@ BG Calibration CGM - Run alarm in %1$d min Bolus reported an error. Manually check real delivered insulin and carb amount Remind for bolus when BG recovers Duration g - Pump suspended Pump running Not configured - Loop suspended Loop suspended by DST req falling rapidly @@ -231,7 +222,6 @@ Closed Loop Open Loop Low Glucose Suspend - Pump disconnected Pump suspended Suspend pump Resume pump @@ -269,11 +259,6 @@ PINs don\'t match - Basal values not aligned to hours: %1$s - Basal value replaced by minimum supported value: %1$s - Basal value replaced by maximum supported value: %1$s - U/h - g/U Start profile %1$d%% for %2$d min @@ -317,7 +302,6 @@ - %1$d min Careportal @@ -504,7 +488,6 @@ Revert to defaults Loop NS - Record Right Chest Left Chest Upper Right Outer Arm @@ -559,7 +542,6 @@ %1$d g additional carbs required within %2$d minutes Basal - Bolus TDD Total Daily Dose @@ -573,7 +555,6 @@ EXTENDED BOLUS SUPERBOLUS TBR CARBS - EXTENDED CARBS TEMP BASAL TEMP TARGET NEW PROFILE @@ -676,22 +657,15 @@ REMOTE CONFIG CHANGED - Profile low target - Profile high target Temporary target bottom value Temporary target top value Temporary target value - Profile DIA value Insulin DIA value Insulin Peak value - Profile sensitivity value Maximal profile basal value Current basal value - Profile carbs ratio value %1$.2f limited to %2$.2f »%1$s« is out of hard limits - »%1$s« %2$.2f is out of hard limits - Basal value No profile selected @@ -755,18 +729,12 @@ Bolus advisor You have high glycemia. Instead of eating now it\'s recommended to wait for better glycemia. Do you want to do a correction bolus now and remind you when it\'s time to eat? In this case no carbs will be recorded and you must use wizard again when we remind you. - COB vs IOB - !!!!! Slow carbs absorption detected: %1$d%% of time. Double check your calculation. COB can be overestimated thus more insulin could be given !!!!! - Bolus constraint applied: %1$.2f U to %2$.2f U - Bolus will be recorded only (not delivered by pump) - Run alarm when is time to eat BG Correction Subtotal Bread, savory pastries, yogurt, fruit (apple, banana, pear), potatoes, rice, buckwheat, oatmeal, slow and low carb foods, vegetables Dumplings, pasta, Asian cuisine rich in carbs and fats, sweet and yeast pastries, cereals, grain porridge, honey, jam Lasagna, pizza, hamburgers, fries, baked potatoes, chips and similar snacks - eCarbs %1$dg / %2$dh (+%3$dmin) Total Carb time Now @@ -1042,7 +1010,6 @@ %1$.1f U - %1$.2f U %1$+.2f U Rate: %1$.2f%% (%2$.2f U/h) Duration: %3$d min Rate: %1$.2f U/h (%2$.2f%%) Duration: %3$d min diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt index da75fe0c56d4..d972f46d9947 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/NumberInputRowPreviews.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.ui.UiStrings import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -10,7 +11,7 @@ import app.aaps.core.keys.interfaces.TextRef @Composable internal fun NumberInputRowBasicPreview() { MaterialTheme { - NumberInputRow(labelRef = UiStrings.carbs, value = 20.0, onValueChange = {}, valueRange = 0.0..100.0, step = 1.0) + NumberInputRow(labelRef = InterfacesStrings.carbs, value = 20.0, onValueChange = {}, valueRange = 0.0..100.0, step = 1.0) } } diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt index 3e44a2492eb7..39d6bd1158c8 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/dialogs/ElementConfirmationDialog.kt @@ -2,7 +2,6 @@ package app.aaps.core.ui.compose.dialogs import app.aaps.core.ui.compose.stringResourceOrNull import app.aaps.core.ui.compose.stringResource -import app.aaps.core.ui.UiStrings import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import app.aaps.core.data.ui.ConfirmationLine diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt index 8033c386ac9d..7f23c3d70308 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/navigation/ElementTypeStyle.kt @@ -1,5 +1,6 @@ package app.aaps.core.ui.compose.navigation +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.keys.interfaces.TextRef import app.aaps.core.ui.UiStrings import androidx.compose.material.icons.Icons @@ -200,7 +201,7 @@ fun ElementCategory.label(): TextRef? = when (this) { fun ElementType.label(): TextRef? = when (this) { ElementType.INSULIN -> UiStrings.overview_insulin_label - ElementType.CARBS -> UiStrings.carbs + ElementType.CARBS -> InterfacesStrings.carbs ElementType.BOLUS_WIZARD -> UiStrings.boluswizard ElementType.QUICK_WIZARD -> null // dynamic label ElementType.QUICK_WIZARD_MANAGEMENT -> UiStrings.quickwizard_managemnt diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt index 464b4ce06eec..2576324d9d94 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/pickers/WeekDaySelector.kt @@ -1,7 +1,6 @@ package app.aaps.core.ui.compose.pickers import app.aaps.core.ui.compose.stringResource -import app.aaps.core.ui.UiStrings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt index 2ac324051208..41d639fe5b7f 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/AdaptiveIntentPreference.kt @@ -4,7 +4,6 @@ package app.aaps.core.ui.compose.preference -import app.aaps.core.ui.UiStrings import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt index cebd7f9f5991..d18fb92fbd24 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/compose/preference/PreferenceSliderWithButtons.kt @@ -1,7 +1,6 @@ package app.aaps.core.ui.compose.preference import app.aaps.core.ui.compose.stringResource -import app.aaps.core.ui.UiStrings import app.aaps.core.keys.interfaces.TextRef import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt index 0e401fda1d4a..1f42c47853a4 100644 --- a/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt +++ b/core/ui/src/commonMain/kotlin/app/aaps/core/ui/extensions/CobInfoDisplay.kt @@ -1,9 +1,9 @@ package app.aaps.core.ui.extensions +import app.aaps.core.interfaces.InterfacesStrings import app.aaps.core.data.iob.CobInfo import app.aaps.core.interfaces.resources.TextResolver import app.aaps.core.interfaces.utils.DecimalFormatter -import app.aaps.core.ui.UiStrings /** * Text for a [CobInfo] on screen. @@ -24,7 +24,7 @@ fun CobInfo.generateCOBString(decimalFormatter: DecimalFormatter): String { fun CobInfo.displayText(rh: TextResolver, decimalFormatter: DecimalFormatter): String? = displayCob?.let { displayCob -> - var cobText = rh.gs(UiStrings.format_carbs, displayCob.toInt()) + var cobText = rh.gs(InterfacesStrings.format_carbs, displayCob.toInt()) if (futureCarbs > 0) cobText += "(" + decimalFormatter.to0Decimal(futureCarbs) + ")" cobText } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt index 3c992f09860a..fa0d4a942f8c 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/bolus/WizardBolusExecutorImpl.kt @@ -51,6 +51,7 @@ import app.aaps.core.objects.wizard.BolusWizard import app.aaps.core.objects.wizard.QuickWizard import app.aaps.core.objects.wizard.QuickWizardEntry import app.aaps.core.ui.R +import app.aaps.core.interfaces.R as InterfacesR import app.aaps.core.ui.compose.formatMinutesAsDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -689,29 +690,29 @@ class WizardBolusExecutorImpl @Inject constructor( val pumpDescription = activePlugin.activePump.pumpDescription val out = mutableListOf() if (insulin > 0.0) { - out += ConfirmationLine(ConfirmationRole.BOLUS, rh.gs(R.string.confirmation_line, rh.gs(R.string.bolus), decimalFormatter.toPumpSupportedBolusWithUnits(insulin, pumpDescription.bolusStep))) + out += ConfirmationLine(ConfirmationRole.BOLUS, rh.gs(InterfacesR.string.confirmation_line, rh.gs(InterfacesR.string.bolus), decimalFormatter.toPumpSupportedBolusWithUnits(insulin, pumpDescription.bolusStep))) if (recordOnly) { - out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(R.string.bolus_recorded_only)) + out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(InterfacesR.string.bolus_recorded_only)) bolus.iCfg?.let { out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.selected_insulin, it.insulinLabel)) } } else if (abs(insulin - bolus.insulin) > pumpDescription.pumpType.determineCorrectBolusStepSize(insulin)) { - out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(R.string.bolus_constraint_applied_warn, bolus.insulin, insulin)) + out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(InterfacesR.string.bolus_constraint_applied_warn, bolus.insulin, insulin)) } } if (carbs != 0) { - out += ConfirmationLine(ConfirmationRole.CARBS, rh.gs(R.string.confirmation_line, rh.gs(R.string.carbs), rh.gs(R.string.format_carbs, carbs))) + out += ConfirmationLine(ConfirmationRole.CARBS, rh.gs(InterfacesR.string.confirmation_line, rh.gs(InterfacesR.string.carbs), rh.gs(InterfacesR.string.format_carbs, carbs))) if (!recordOnly && carbs != bolus.carbs) out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(R.string.constraint_applied)) // Delayed/extended carbs (e.g. wear eCarbs): show the scheduled start time on the general line so every // surface (phone, client, watch) renders it identically — the one piece of info added to the shared path. if (bolus.carbsTimeOffsetMinutes != 0) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.time), dateUtil.timeString(dateUtil.now() + T.mins(bolus.carbsTimeOffsetMinutes.toLong()).msecs()))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.time), dateUtil.timeString(dateUtil.now() + T.mins(bolus.carbsTimeOffsetMinutes.toLong()).msecs()))) if (bolus.carbsDurationHours > 0) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.value_with_unit, bolus.carbsDurationHours.toString(), rh.gs(app.aaps.core.interfaces.R.string.shorthour)))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.value_with_unit, bolus.carbsDurationHours.toString(), rh.gs(app.aaps.core.interfaces.R.string.shorthour)))) } if (bolus.eCarbsGrams > 0) - out += ConfirmationLine(ConfirmationRole.CARBS, rh.gs(R.string.wizard_ecarbs, bolus.eCarbsGrams, bolus.eCarbsDurationHours, bolus.eCarbsDelayMinutes)) + out += ConfirmationLine(ConfirmationRole.CARBS, rh.gs(InterfacesR.string.wizard_ecarbs, bolus.eCarbsGrams, bolus.eCarbsDurationHours, bolus.eCarbsDelayMinutes)) if (bolus.notes.isNotEmpty()) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.notes_label), bolus.notes)) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.notes_label), bolus.notes)) return out } @@ -724,7 +725,7 @@ class WizardBolusExecutorImpl @Inject constructor( private fun buildTempTargetLines(reasonDisplay: String, lowMgdl: Double, highMgdl: Double, durationMinutes: Int, standalone: Boolean = false): List { val out = mutableListOf() if (durationMinutes == 0) { - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.temporary_target), rh.gs(R.string.cancel))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.temporary_target), rh.gs(R.string.cancel))) } else { val units = profileFunction.getUnits() val unitLabel = if (units == GlucoseUnit.MMOL) rh.gs(R.string.mmol) else rh.gs(R.string.mgdl) @@ -734,14 +735,14 @@ class WizardBolusExecutorImpl @Inject constructor( val targetLabel = if (standalone) rh.gs(R.string.target_label) else rh.gs(R.string.temporary_target) out += ConfirmationLine( ConfirmationRole.TEMP_TARGET, - rh.gs(R.string.confirmation_line, targetLabel, rh.gs(R.string.value_with_unit, target, unitLabel)) + rh.gs(InterfacesR.string.confirmation_line, targetLabel, rh.gs(R.string.value_with_unit, target, unitLabel)) ) out += ConfirmationLine( ConfirmationRole.NORMAL, - rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), durationText) + rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), durationText) ) if (reasonDisplay.isNotEmpty()) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.reason), reasonDisplay)) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.reason), reasonDisplay)) } return out } @@ -802,12 +803,12 @@ class WizardBolusExecutorImpl @Inject constructor( /** The PS line(s) for any batch (wear / client / phone) — target profile name + percentage + optional time-shift + duration. */ private suspend fun buildPsLine(ps: BatchAction.ProfileSwitch): List { val out = mutableListOf() - out += ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(R.string.confirmation_line, rh.gs(R.string.profile), ps.profileName ?: profileFunction.getOriginalProfileName())) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.percentage_label), rh.gs(R.string.format_percent, ps.percentage))) + out += ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.profile), ps.profileName ?: profileFunction.getOriginalProfileName())) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.percentage_label), rh.gs(R.string.format_percent, ps.percentage))) if (ps.timeShiftHours != 0) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.timeshift_label), rh.gs(R.string.value_with_unit, ps.timeShiftHours.toString(), rh.gs(app.aaps.core.interfaces.R.string.shorthour)))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.timeshift_label), rh.gs(R.string.value_with_unit, ps.timeShiftHours.toString(), rh.gs(app.aaps.core.interfaces.R.string.shorthour)))) if (ps.durationMinutes > 0) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), formatMinutesAsDuration(ps.durationMinutes, rh))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), formatMinutesAsDuration(ps.durationMinutes, rh))) return out } @@ -861,7 +862,7 @@ class WizardBolusExecutorImpl @Inject constructor( val out = mutableListOf() out += ConfirmationLine(rmModeRole(rm.mode), rmModeTitle(rm.mode)) if (rm.durationMinutes > 0) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), formatMinutesAsDuration(rm.durationMinutes, rh))) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), formatMinutesAsDuration(rm.durationMinutes, rh))) return out } @@ -883,7 +884,7 @@ class WizardBolusExecutorImpl @Inject constructor( RM.Mode.OPEN_LOOP -> rh.gs(R.string.openloop) RM.Mode.DISABLED_LOOP -> rh.gs(R.string.disableloop) RM.Mode.SUSPENDED_BY_USER -> rh.gs(R.string.suspendloop) - RM.Mode.DISCONNECTED_PUMP -> rh.gs(R.string.pump_disconnected) + RM.Mode.DISCONNECTED_PUMP -> rh.gs(InterfacesR.string.pump_disconnected) RM.Mode.RESUME -> if (loop.runningMode() == RM.Mode.DISCONNECTED_PUMP) rh.gs(R.string.pump_reconnect) else rh.gs(R.string.resumeloop) RM.Mode.SUPER_BOLUS, RM.Mode.SUSPENDED_BY_PUMP, RM.Mode.SUSPENDED_BY_DST -> rh.gs(R.string.running_mode) } @@ -905,8 +906,8 @@ class WizardBolusExecutorImpl @Inject constructor( private fun buildTempBasalLine(capped: BatchAction.TempBasal, original: BatchAction.TempBasal?): List { val out = mutableListOf() val rateStr = if (capped.isPercent) rh.gs(R.string.format_percent, capped.rate.toInt()) else rh.gs(R.string.pump_base_basal_rate, capped.rate) - out += ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(R.string.confirmation_line, rh.gs(R.string.tempbasal_label), rateStr)) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.format_mins, capped.durationMinutes))) + out += ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.tempbasal_label), rateStr)) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.format_mins, capped.durationMinutes))) if (original != null && capped.rate != original.rate) out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(R.string.constraint_applied)) return out } @@ -921,8 +922,8 @@ class WizardBolusExecutorImpl @Inject constructor( /** The extended-bolus line(s) — insulin + duration, with a cap warning when reduced. */ private fun buildExtendedBolusLine(capped: BatchAction.ExtendedBolus, original: BatchAction.ExtendedBolus?): List { val out = mutableListOf() - out += ConfirmationLine(ConfirmationRole.BOLUS, rh.gs(R.string.format_insulin_units, capped.insulin)) - out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(R.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.format_mins, capped.durationMinutes))) + out += ConfirmationLine(ConfirmationRole.BOLUS, rh.gs(InterfacesR.string.format_insulin_units, capped.insulin)) + out += ConfirmationLine(ConfirmationRole.NORMAL, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.duration), rh.gs(R.string.format_mins, capped.durationMinutes))) if (original != null && abs(capped.insulin - original.insulin) > 0.01) out += ConfirmationLine(ConfirmationRole.WARNING, rh.gs(R.string.constraint_applied)) return out } @@ -943,11 +944,11 @@ class WizardBolusExecutorImpl @Inject constructor( /** The single cancel line — "Cancel: Temp basal" / "Cancel: Extended bolus" ([labelRes] = the cancelled action). */ private fun buildCancelLine(labelRes: Int): List = - listOf(ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(R.string.confirmation_line, rh.gs(R.string.cancel), rh.gs(labelRes)))) + listOf(ConfirmationLine(ConfirmationRole.PRIMARY, rh.gs(InterfacesR.string.confirmation_line, rh.gs(R.string.cancel), rh.gs(labelRes)))) /** The insulin-activate line — "Activate insulin: