diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 0f2dbc5e0cf9..000000000000 --- a/PLAN.md +++ /dev/null @@ -1,58 +0,0 @@ -# RileyLink Pairing Wizard — Compose Migration - -## Goal - -Replace `RileyLinkBLEConfigActivity` (legacy Activity) with a shared Compose wizard using existing -`BleScanStep` + `WizardScreen` infrastructure. Add "Pair RileyLink" button to Eros overview. -Remove RL pairing from preferences. - -## Scope - -- Shared wizard in `pump:rileylink` module -- Integration into Eros overview (already Compose) -- Medtronic integration deferred (separate plan) - -## Phase 0: Prerequisites - -- [ ] Create `RileyLinkBleScanner` implementing `BleScanner` interface - - UUID filter: `GattAttributes.SERVICE_RADIO` - - Disconnect current RL before scanning (so it's discoverable) - - Reconnect on stop - - 15s scan timeout - - File: `pump/rileylink/.../ble/RileyLinkBleScanner.kt` - -## Phase 1: Wizard ViewModel - -- [ ] Create `RileyLinkPairWizardViewModel` in `pump/rileylink/.../compose/` - - Collect `scannedDevices` from `RileyLinkBleScanner` - - On device selected: save MAC + name to preferences, call `verifyConfiguration(true)`, - trigger pump config changed event - - No remove action — user just pairs a new device to replace - - File: `pump/rileylink/.../compose/RileyLinkPairWizardViewModel.kt` - -## Phase 2: Wizard Screen - -- [ ] Create `RileyLinkPairWizardScreen` composable - - Single step: `BleScanStep` — scan for devices, tap to select - - On device tap: save MAC + name, trigger reconnect, close wizard - - No finish/confirm step needed — just pick and done - - File: `pump/rileylink/.../compose/RileyLinkPairWizardScreen.kt` - -## Phase 3: Eros Integration - -- [ ] Add "Pair RileyLink" button to `ErosOverviewViewModel.buildManagementActions()` -- [ ] Add `ShowRileyLinkPairWizard` event to `OmnipodOverviewEvent` -- [ ] Handle wizard navigation in `OmnipodErosComposeContent` - - New `showRileyLinkPairWizard` state, render wizard when true - -## Phase 4: Cleanup - -- [ ] Remove `RileyLinkBLEConfigActivity` preference integration from `OmnipodErosPumpPlugin` - - Remove `RileyLinkIntentPreferenceKey.MacAddressSelector` from Eros preferences -- [ ] Verify RL pairing still works end-to-end (user test) - -## Notes - -- `BleScanStep` needs no changes — UUID filtering happens in scanner, not UI -- `BleScanner` interface needs no changes — each pump provides own implementation -- `RileyLinkBLEConfigActivity` kept alive until Medtronic also migrated, then deleted 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 3dc21e2e0d6d..a847e837c19c 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 @@ -9,7 +9,6 @@ import app.aaps.core.keys.interfaces.IntentPreferenceKey * - XdripIntentKey in :plugins:sync * - SmsIntentKey in :plugins:main * - OverviewIntentKey in :plugins:main - * - RileyLinkIntentPreferenceKey in :pump:rileylink * * This enum is kept for backwards compatibility but should remain empty. * New intent keys should be added to their respective module key enums. 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 3a1dcf0cf768..d19afc516c57 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 @@ -3,6 +3,7 @@ package app.aaps.core.ui.compose.pump import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -86,11 +87,11 @@ private fun CommunicationStatusCard(banner: StatusBanner?, queueStatus: String?) if (banner == null && queueStatus == null) return val (bgColor, fgColor) = when (banner?.level) { - StatusLevel.CRITICAL -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer - StatusLevel.WARNING -> MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer - StatusLevel.NORMAL -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + StatusLevel.CRITICAL -> MaterialTheme.colorScheme.errorContainer to MaterialTheme.colorScheme.onErrorContainer + StatusLevel.WARNING -> MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer + StatusLevel.NORMAL -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer StatusLevel.UNSPECIFIED, - null -> MaterialTheme.colorScheme.surfaceContainerHigh to MaterialTheme.colorScheme.onSurface + null -> MaterialTheme.colorScheme.surfaceContainerHigh to MaterialTheme.colorScheme.onSurface } Surface( @@ -181,7 +182,8 @@ private fun ActionButtons(actions: List) { FilledTonalButton( onClick = action.onClick, enabled = action.enabled, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp) ) { if (action.icon != null) { Icon( @@ -197,8 +199,8 @@ private fun ActionButtons(actions: List) { modifier = Modifier.size(18.dp) ) } - Spacer(modifier = Modifier.size(8.dp)) - Text(text = action.label) + Spacer(modifier = Modifier.size(4.dp)) + Text(text = action.label, maxLines = 1) } } if (row.size == 1) { diff --git a/pump/medtronic/build.gradle.kts b/pump/medtronic/build.gradle.kts index 776264457b75..b5d6a990afcb 100644 --- a/pump/medtronic/build.gradle.kts +++ b/pump/medtronic/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { implementation(project(":pump:common")) implementation(project(":pump:rileylink")) + implementation(libs.androidx.hilt.navigation.compose) + testImplementation(project(":core:keys")) testImplementation(project(":shared:tests")) diff --git a/pump/medtronic/src/main/AndroidManifest.xml b/pump/medtronic/src/main/AndroidManifest.xml index 935c304195de..154c28bcc5e1 100644 --- a/pump/medtronic/src/main/AndroidManifest.xml +++ b/pump/medtronic/src/main/AndroidManifest.xml @@ -6,11 +6,6 @@ android:enabled="true" android:exported="false" /> - - diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicFragment.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicFragment.kt deleted file mode 100644 index 42eaf783fd3b..000000000000 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/MedtronicFragment.kt +++ /dev/null @@ -1,373 +0,0 @@ -package app.aaps.pump.medtronic - -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import app.aaps.core.data.model.EB -import app.aaps.core.data.model.TB -import app.aaps.core.data.time.T -import app.aaps.core.interfaces.db.PersistenceLayer -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.pump.PumpSync -import app.aaps.core.interfaces.pump.WarnColors -import app.aaps.core.interfaces.pump.defs.PumpDeviceState -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.events.EventPumpStatusChanged -import app.aaps.core.interfaces.rx.events.EventQueueChanged -import app.aaps.core.interfaces.rx.events.EventRefreshButtonState -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.pump.common.events.EventRileyLinkDeviceStatusChange -import app.aaps.pump.common.extensions.stringResource -import app.aaps.pump.common.hw.rileylink.defs.RileyLinkServiceState -import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice -import app.aaps.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity -import app.aaps.pump.common.hw.rileylink.service.RileyLinkServiceData -import app.aaps.pump.medtronic.databinding.MedtronicFragmentBinding -import app.aaps.pump.medtronic.defs.BatteryType -import app.aaps.pump.medtronic.defs.MedtronicCommandType -import app.aaps.pump.medtronic.dialog.MedtronicHistoryActivity -import app.aaps.pump.medtronic.driver.MedtronicPumpStatus -import app.aaps.pump.medtronic.events.EventMedtronicPumpConfigurationChanged -import app.aaps.pump.medtronic.events.EventMedtronicPumpValuesChanged -import app.aaps.pump.medtronic.util.MedtronicUtil -import dagger.android.support.DaggerFragment -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import java.util.Locale -import javax.inject.Inject - -class MedtronicFragment : DaggerFragment() { - - @Inject lateinit var aapsLogger: AAPSLogger - @Inject lateinit var fabricPrivacy: FabricPrivacy - @Inject lateinit var rh: ResourceHelper - @Inject lateinit var dateUtil: DateUtil - @Inject lateinit var rxBus: RxBus - @Inject lateinit var commandQueue: CommandQueue - @Inject lateinit var activePlugin: ActivePlugin - @Inject lateinit var medtronicPumpPlugin: MedtronicPumpPlugin - @Inject lateinit var warnColors: WarnColors - @Inject lateinit var medtronicUtil: MedtronicUtil - @Inject lateinit var medtronicPumpStatus: MedtronicPumpStatus - @Inject lateinit var rileyLinkServiceData: RileyLinkServiceData - @Inject lateinit var aapsSchedulers: AapsSchedulers - @Inject lateinit var pumpSync: PumpSync - @Inject lateinit var uiInteraction: UiInteraction - @Inject lateinit var persistenceLayer: PersistenceLayer - - private var disposable: CompositeDisposable = CompositeDisposable() - - private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) - private var refreshLoop: Runnable - - init { - refreshLoop = Runnable { - activity?.runOnUiThread { updateGUI() } - handler.postDelayed(refreshLoop, T.mins(1).msecs()) - } - } - - private var _binding: MedtronicFragmentBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - MedtronicFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.rlStatus.text = rh.gs(RileyLinkServiceState.NotStarted.resourceId) - - binding.pumpStatusIcon.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor)) - @SuppressLint("SetTextI18n") - binding.pumpStatusIcon.text = "{fa-bed}" - - binding.history.setOnClickListener { - if (medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { - startActivity(Intent(context, MedtronicHistoryActivity::class.java)) - } else { - displayNotConfiguredDialog() - } - } - - binding.refresh.setOnClickListener { - if (medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { - displayNotConfiguredDialog() - } else { - binding.refresh.isEnabled = false - medtronicPumpPlugin.resetStatusState() - commandQueue.readStatus(rh.gs(R.string.clicked_refresh), object : Callback() { - override fun run() { - activity?.runOnUiThread { if (_binding != null) binding.refresh.isEnabled = true } - } - }) - } - } - - binding.stats.setOnClickListener { - if (medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { - startActivity(Intent(context, RileyLinkStatusActivity::class.java)) - } else { - displayNotConfiguredDialog() - } - } - } - - @Synchronized - override fun onResume() { - super.onResume() - handler.postDelayed(refreshLoop, T.mins(1).msecs()) - disposable += rxBus - .toObservable(EventRefreshButtonState::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ binding.refresh.isEnabled = it.newState }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventRileyLinkDeviceStatusChange::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ - aapsLogger.debug(LTag.PUMP, "onStatusEvent(EventRileyLinkDeviceStatusChange): $it") - setDeviceStatus() - }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventMedtronicPumpValuesChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ updateGUI() }, fabricPrivacy::logException) - persistenceLayer.observeChanges(EB::class.java) - .onEach { updateGUI() }.launchIn(viewLifecycleOwner.lifecycleScope) - persistenceLayer.observeChanges(TB::class.java) - .onEach { updateGUI() }.launchIn(viewLifecycleOwner.lifecycleScope) - disposable += rxBus - .toObservable(EventMedtronicPumpConfigurationChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ - aapsLogger.debug(LTag.PUMP, "EventMedtronicPumpConfigurationChanged triggered") - medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() - updateGUI() - }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventPumpStatusChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ updateGUI() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventQueueChanged::class.java) - .observeOn(aapsSchedulers.main) - .subscribe({ updateGUI() }, fabricPrivacy::logException) - - updateGUI() - } - - @Synchronized - override fun onPause() { - super.onPause() - disposable.clear() - handler.removeCallbacksAndMessages(null) - } - - @Synchronized - override fun onDestroy() { - super.onDestroy() - handler.removeCallbacksAndMessages(null) - handler.looper.quitSafely() - } - - @Synchronized - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - @SuppressLint("SetTextI18n") - @Synchronized - private fun setDeviceStatus() { - val resourceId = rileyLinkServiceData.rileyLinkServiceState.resourceId - val rileyLinkError = rileyLinkServiceData.rileyLinkError - binding.rlStatus.text = - when { - rileyLinkServiceData.rileyLinkServiceState == RileyLinkServiceState.NotStarted -> rh.gs(resourceId) - rileyLinkServiceData.rileyLinkServiceState.isConnecting() -> "{fa-bluetooth-b spin} " + rh.gs(resourceId) - rileyLinkServiceData.rileyLinkServiceState.isError() && rileyLinkError == null -> "{fa-bluetooth-b} " + rh.gs(resourceId) - rileyLinkServiceData.rileyLinkServiceState.isError() && rileyLinkError != null -> "{fa-bluetooth-b} " + rh.gs(rileyLinkError.getResourceId(RileyLinkTargetDevice.MedtronicPump)) - else -> "{fa-bluetooth-b} " + rh.gs(resourceId) - } - binding.rlStatus.setTextColor(rh.gac(context, if (rileyLinkError != null) app.aaps.core.ui.R.attr.warningColor else app.aaps.core.ui.R.attr.defaultTextColor)) - - binding.errors.text = - rileyLinkServiceData.rileyLinkError?.let { - rh.gs(it.getResourceId(RileyLinkTargetDevice.MedtronicPump)) - } ?: "-" - - when (medtronicPumpStatus.pumpDeviceState) { - PumpDeviceState.Sleeping -> - binding.pumpStatusIcon.text = "{fa-bed} " // + pumpStatus.pumpDeviceState.name()); - - PumpDeviceState.NeverContacted, - PumpDeviceState.WakingUp, - PumpDeviceState.PumpUnreachable, - PumpDeviceState.ErrorWhenCommunicating, - PumpDeviceState.TimeoutWhenCommunicating, - PumpDeviceState.InvalidConfiguration -> - binding.pumpStatusIcon.text = " " + rh.gs(medtronicPumpStatus.pumpDeviceState.stringResource()) - - PumpDeviceState.Active -> { - val cmd = medtronicUtil.getCurrentCommand() - if (cmd == null) - binding.pumpStatusIcon.text = " " + rh.gs(medtronicPumpStatus.pumpDeviceState.stringResource()) - else { - aapsLogger.debug(LTag.PUMP, "Command: $cmd") - val cmdResourceId = cmd.resourceId //!! - if (cmd == MedtronicCommandType.GetHistoryData) { - binding.pumpStatusIcon.text = medtronicUtil.frameNumber?.let { - rh.gs(cmdResourceId!!, medtronicUtil.pageNumber, medtronicUtil.frameNumber) - } - ?: rh.gs(R.string.medtronic_cmd_desc_get_history_request, medtronicUtil.pageNumber) - } else { - binding.pumpStatusIcon.text = " " + (cmdResourceId?.let { rh.gs(it) } - ?: cmd.commandDescription) - } - } - } - - // else -> - // aapsLogger.warn(LTag.PUMP, "Unknown pump state: " + medtronicPumpStatus.pumpDeviceState) - } - - val status = commandQueue.spannedStatus() - if (status.toString() == "") { - binding.queue.visibility = View.GONE - } else { - binding.queue.visibility = View.VISIBLE - binding.queue.text = status - } - } - - private fun displayNotConfiguredDialog() { - uiInteraction.showOkDialog( - context = requireActivity(), - title = rh.gs(R.string.medtronic_warning), - message = rh.gs(R.string.medtronic_error_operation_not_possible_no_configuration) - ) - } - - // GUI functions - @SuppressLint("SetTextI18n") - @Synchronized - fun updateGUI() { - if (_binding == null) return - - setDeviceStatus() - - // last connection - if (medtronicPumpStatus.lastConnection != 0L) { - val minAgo = dateUtil.minAgo(rh, medtronicPumpStatus.lastConnection) - val min = (System.currentTimeMillis() - medtronicPumpStatus.lastConnection) / 1000 / 60 - if (medtronicPumpStatus.lastConnection + 60 * 1000 > System.currentTimeMillis()) { - binding.lastConnection.setText(R.string.medtronic_pump_connected_now) - binding.lastConnection.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor)) - } else if (medtronicPumpStatus.lastConnection + 30 * 60 * 1000 < System.currentTimeMillis()) { - - if (min < 60) { - binding.lastConnection.text = rh.gs(app.aaps.core.interfaces.R.string.minago, min) - } else if (min < 1440) { - val h = (min / 60).toInt() - binding.lastConnection.text = (rh.gq(app.aaps.pump.common.hw.rileylink.R.plurals.duration_hours, h, h) + " " - + rh.gs(R.string.ago)) - } else { - val h = (min / 60).toInt() - val d = h / 24 - // h = h - (d * 24); - binding.lastConnection.text = (rh.gq(app.aaps.pump.common.hw.rileylink.R.plurals.duration_days, d, d) + " " - + rh.gs(R.string.ago)) - } - binding.lastConnection.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.warningColor)) - } else { - binding.lastConnection.text = minAgo - binding.lastConnection.setTextColor(rh.gac(context, app.aaps.core.ui.R.attr.defaultTextColor)) - } - } - - // last bolus - val bolus = medtronicPumpStatus.lastBolusAmount - val bolusTime = medtronicPumpStatus.lastBolusTime - if (bolus != null && bolusTime != null) { - val agoMsc = System.currentTimeMillis() - bolusTime.time - val bolusMinAgo = agoMsc.toDouble() / 60.0 / 1000.0 - val unit = rh.gs(app.aaps.core.ui.R.string.insulin_unit_shortname) - val ago = when { - agoMsc < 60 * 1000 -> rh.gs(R.string.medtronic_pump_connected_now) - bolusMinAgo < 60 -> dateUtil.minAgo(rh, bolusTime.time) - else -> dateUtil.hourAgo(bolusTime.time, rh) - } - binding.lastBolus.text = rh.gs(R.string.mdt_last_bolus, bolus, unit, ago) - } else { - binding.lastBolus.text = "" - } - - // base basal rate - binding.baseBasalRate.text = ("(" + medtronicPumpStatus.activeProfileName + ") " - + rh.gs(app.aaps.core.ui.R.string.pump_base_basal_rate, medtronicPumpPlugin.baseBasalRate.cU)) - - // TBR - var tbrStr = "" - val tbrRemainingTime: Int? = medtronicPumpStatus.tbrRemainingTime - - if (tbrRemainingTime != null) { - tbrStr = rh.gs(R.string.mdt_tbr_remaining, medtronicPumpStatus.tempBasalAmount, tbrRemainingTime) - } - binding.tempBasal.text = tbrStr - - // battery - if (medtronicPumpStatus.batteryType == BatteryType.None || medtronicPumpStatus.batteryVoltage == null) { - binding.pumpStateBattery.text = medtronicPumpStatus.batteryRemaining?.let { "{fa-battery-" + it / 25 + "}" } ?: rh.gs(app.aaps.core.ui.R.string.unknown) - } else { - binding.pumpStateBattery.text = - (medtronicPumpStatus.batteryRemaining?.let { "{fa-battery-" + it / 25 + "} " + it + "%" } ?: "") + - String.format(Locale.getDefault(), " (%.2f V)", medtronicPumpStatus.batteryVoltage) - - } - warnColors.setColorInverse(binding.pumpStateBattery, (medtronicPumpStatus.batteryRemaining?.toDouble() ?: 100.0), 25, 10) - - // reservoir - binding.reservoir.text = rh.gs(app.aaps.core.ui.R.string.reservoir_value, medtronicPumpStatus.reservoirRemainingUnits, medtronicPumpStatus.reservoirFullUnits) - warnColors.setColorInverse(binding.reservoir, medtronicPumpStatus.reservoirRemainingUnits, 50, 20) - - medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() - binding.errors.text = medtronicPumpStatus.errorInfo - - if (rileyLinkServiceData.showBatteryLevel) { - binding.rlBatteryView.visibility = View.VISIBLE - binding.rlBatteryLabel.visibility = View.VISIBLE - binding.rlBatteryState.visibility = View.VISIBLE - binding.rlBatteryLayout.visibility = View.VISIBLE - binding.rlBatterySemicolon.visibility = View.VISIBLE - binding.rlBatteryState.text = - if (rileyLinkServiceData.batteryLevel == null) " ?" - else "{fa-battery-${rileyLinkServiceData.batteryLevel!! / 25}} ${rileyLinkServiceData.batteryLevel}%" - } else { - binding.rlBatteryView.visibility = View.GONE - binding.rlBatteryLabel.visibility = View.GONE - binding.rlBatteryState.visibility = View.GONE - binding.rlBatteryLayout.visibility = View.GONE - binding.rlBatterySemicolon.visibility = View.GONE - } - - } -} 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 ef94be54f5a8..beba4c37651f 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 @@ -2,7 +2,6 @@ package app.aaps.pump.medtronic import android.content.ComponentName import android.content.Context -import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import android.os.SystemClock @@ -21,6 +20,7 @@ import app.aaps.core.interfaces.notifications.NotificationId import app.aaps.core.interfaces.notifications.NotificationManager import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.profile.Profile +import app.aaps.core.interfaces.pump.BlePreCheck import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.Pump import app.aaps.core.interfaces.pump.PumpEnactResult @@ -28,8 +28,6 @@ import app.aaps.core.interfaces.pump.PumpProfile import app.aaps.core.interfaces.pump.PumpRate import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.PumpSync.TemporaryBasalType -import app.aaps.core.interfaces.pump.actions.CustomAction -import app.aaps.core.interfaces.pump.actions.CustomActionType import app.aaps.core.interfaces.pump.defs.determineCorrectBasalSize import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.resources.ResourceHelper @@ -49,7 +47,6 @@ import app.aaps.core.utils.DateTimeUtil import app.aaps.core.validators.DefaultEditTextValidator import app.aaps.core.validators.EditTextValidator import app.aaps.core.validators.preferences.AdaptiveIntPreference -import app.aaps.core.validators.preferences.AdaptiveIntentPreference import app.aaps.core.validators.preferences.AdaptiveListIntPreference import app.aaps.core.validators.preferences.AdaptiveListPreference import app.aaps.core.validators.preferences.AdaptiveStringPreference @@ -57,7 +54,6 @@ import app.aaps.core.validators.preferences.AdaptiveSwitchPreference import app.aaps.pump.common.PumpPluginAbstract import app.aaps.pump.common.data.PumpStatus import app.aaps.pump.common.defs.PumpDriverState -import app.aaps.pump.common.dialog.RileyLinkBLEConfigActivity import app.aaps.pump.common.driver.refresh.PumpDataRefreshAction import app.aaps.pump.common.driver.refresh.PumpDataRefreshType import app.aaps.pump.common.events.EventRileyLinkDeviceStatusChange @@ -67,7 +63,6 @@ import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpDevice import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpInfo import app.aaps.pump.common.hw.rileylink.defs.RileyLinkServiceState import app.aaps.pump.common.hw.rileylink.keys.RileyLinkDoubleKey -import app.aaps.pump.common.hw.rileylink.keys.RileyLinkIntentPreferenceKey import app.aaps.pump.common.hw.rileylink.keys.RileyLinkLongKey import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringKey import app.aaps.pump.common.hw.rileylink.keys.RileyLinkStringPreferenceKey @@ -82,6 +77,7 @@ import app.aaps.pump.common.sync.PumpSyncStorage import app.aaps.pump.common.utils.ProfileUtil import app.aaps.pump.medtronic.comm.history.pump.PumpHistoryEntry import app.aaps.pump.medtronic.comm.history.pump.PumpHistoryResult +import app.aaps.pump.medtronic.compose.MedtronicComposeContent import app.aaps.pump.medtronic.data.MedtronicHistoryData import app.aaps.pump.medtronic.data.dto.BasalProfile import app.aaps.pump.medtronic.data.dto.BasalProfile.Companion.getProfilesByHourToString @@ -91,7 +87,6 @@ import app.aaps.pump.medtronic.defs.BasalProfileStatus import app.aaps.pump.medtronic.defs.BatteryType import app.aaps.pump.medtronic.defs.MedtronicCommandType import app.aaps.pump.medtronic.defs.MedtronicCommandType.Companion.getSettings -import app.aaps.pump.medtronic.defs.MedtronicCustomActionType import app.aaps.pump.medtronic.defs.MedtronicDeviceType import app.aaps.pump.medtronic.defs.MedtronicNotificationType import app.aaps.pump.medtronic.defs.MedtronicUIResponseType @@ -152,18 +147,24 @@ class MedtronicPumpPlugin @Inject constructor( decimalFormatter: DecimalFormatter, pumpEnactResultProvider: Provider, private val wakeAndTuneTaskProvider: Provider, - private val resetRileyLinkConfigurationTaskProvider: Provider + private val resetRileyLinkConfigurationTaskProvider: Provider, + private val blePreCheck: BlePreCheck ) : PumpPluginAbstract( pluginDescription = PluginDescription() .mainType(PluginType.PUMP) - .fragmentClass(MedtronicFragment::class.java.name) + .composeContent { _ -> + MedtronicComposeContent( + pluginName = rh.gs(R.string.medtronic_name), + blePreCheck = blePreCheck + ) + } .icon(IcPluginMedtronic) .pluginName(R.string.medtronic_name) .shortName(R.string.medtronic_name_short) .preferencesId(PluginDescription.PREFERENCE_SCREEN) .description(R.string.description_pump_medtronic), ownPreferences = listOf( - RileylinkBooleanPreferenceKey::class.java, RileyLinkDoubleKey::class.java, RileyLinkIntentPreferenceKey::class.java, + 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 @@ -349,7 +350,6 @@ class MedtronicPumpPlugin @Inject constructor( } if (deleteFromQueue.size == busyTimestamps.size) { busyTimestamps.clear() - setEnableCustomAction(MedtronicCustomActionType.ClearBolusBlock, false) } if (deleteFromQueue.isNotEmpty()) { busyTimestamps.removeAll(deleteFromQueue) @@ -711,7 +711,6 @@ class MedtronicPumpPlugin @Inject constructor( val bolusTime = (detailedBolusInfo.insulin * 42.0).toInt() val time = now + bolusTime * 1000 busyTimestamps.add(time) - setEnableCustomAction(MedtronicCustomActionType.ClearBolusBlock, true) pumpEnactResultProvider.get().success(true).enacted(true).bolusDelivered(detailedBolusInfo.insulin) } } finally { @@ -1204,57 +1203,6 @@ class MedtronicPumpPlugin @Inject constructor( return basalProfile } - // OPERATIONS not supported by Pump or Plugin - private var customActions: List? = null - private val customActionWakeUpAndTune = CustomAction( - R.string.medtronic_custom_action_wake_and_tune, - MedtronicCustomActionType.WakeUpAndTune, - app.aaps.core.ui.R.drawable.ic_actions_profileswitch - ) - private val customActionClearBolusBlock = CustomAction( - R.string.medtronic_custom_action_clear_bolus_block, MedtronicCustomActionType.ClearBolusBlock, app.aaps.core.ui.R.drawable.ic_actions_profileswitch, false - ) - private val customActionResetRLConfig = CustomAction( - R.string.medtronic_custom_action_reset_rileylink, MedtronicCustomActionType.ResetRileyLinkConfiguration, app.aaps.core.ui.R.drawable.ic_actions_profileswitch, true - ) - - override fun getCustomActions(): List? { - if (customActions == null) { - customActions = listOf( - customActionWakeUpAndTune, // - customActionClearBolusBlock, // - customActionResetRLConfig - ) - } - return customActions - } - - override fun executeCustomAction(customActionType: CustomActionType) { - when (customActionType as? MedtronicCustomActionType) { - MedtronicCustomActionType.WakeUpAndTune -> { - if (rileyLinkMedtronicService?.verifyConfiguration() == true) { - serviceTaskExecutor.startTask(wakeAndTuneTaskProvider.get()) - } else { - uiInteraction.runAlarm(rh.gs(R.string.medtronic_error_operation_not_possible_no_configuration), rh.gs(R.string.medtronic_warning), app.aaps.core.ui.R.raw.boluserror) - } - } - - MedtronicCustomActionType.ClearBolusBlock -> { - busyTimestamps.clear() - customActionClearBolusBlock.isEnabled = false - refreshCustomActionsList() - } - - MedtronicCustomActionType.ResetRileyLinkConfiguration -> { - serviceTaskExecutor.startTask(resetRileyLinkConfigurationTaskProvider.get()) - } - - null -> { // do nothing - - } - } - } - override fun timezoneOrDSTChanged(timeChangeType: TimeChangeType) { aapsLogger.warn(LTag.PUMP, "Time or TimeZone changed. ") hasTimeDateOrTimeZoneChanged = true @@ -1264,14 +1212,10 @@ class MedtronicPumpPlugin @Inject constructor( return preferences.get(MedtronicBooleanPreferenceKey.SetNeutralTemp) } - @Suppress("SameParameterValue") - private fun setEnableCustomAction(customAction: MedtronicCustomActionType, isEnabled: Boolean) { - if (customAction === MedtronicCustomActionType.ClearBolusBlock) { - customActionClearBolusBlock.isEnabled = isEnabled - } else if (customAction === MedtronicCustomActionType.ResetRileyLinkConfiguration) { - customActionResetRLConfig.isEnabled = isEnabled - } - refreshCustomActionsList() + fun isBusyBlockingEnabled(): Boolean = busyTimestamps.isNotEmpty() + + fun clearBusyTimestamps() { + busyTimestamps.clear() } companion object { @@ -1296,7 +1240,6 @@ class MedtronicPumpPlugin @Inject constructor( MedtronicIntPreferenceKey.BolusDelay, RileyLinkStringPreferenceKey.Encoding, MedtronicStringPreferenceKey.BatteryType, - RileyLinkIntentPreferenceKey.MacAddressSelector, RileylinkBooleanPreferenceKey.OrangeUseScanning, RileylinkBooleanPreferenceKey.ShowReportedBatteryLevel, MedtronicBooleanPreferenceKey.SetNeutralTemp @@ -1392,12 +1335,6 @@ class MedtronicPumpPlugin @Inject constructor( entryValues = batteryValues ) ) - addPreference( - AdaptiveIntentPreference( - ctx = context, intentKey = RileyLinkIntentPreferenceKey.MacAddressSelector, title = app.aaps.pump.common.hw.rileylink.R.string.rileylink_configuration, - intent = Intent(context, RileyLinkBLEConfigActivity::class.java) - ) - ) addPreference( AdaptiveSwitchPreference( ctx = context, diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicComposeContent.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicComposeContent.kt new file mode 100644 index 000000000000..8630d8483bbb --- /dev/null +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicComposeContent.kt @@ -0,0 +1,201 @@ +package app.aaps.pump.medtronic.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.aaps.core.interfaces.pump.BlePreCheck +import app.aaps.core.ui.compose.ComposablePluginContent +import app.aaps.core.ui.compose.LocalSnackbarHostState +import app.aaps.core.ui.compose.ToolbarConfig +import app.aaps.core.ui.compose.dialogs.OkDialog +import app.aaps.core.ui.compose.pump.BlePreCheckHost +import app.aaps.core.ui.compose.pump.PumpOverviewScreen +import app.aaps.pump.common.compose.RileyLinkPairWizardEvent +import app.aaps.pump.common.compose.RileyLinkPairWizardScreen +import app.aaps.pump.common.compose.RileyLinkPairWizardViewModel +import app.aaps.pump.common.compose.RileyLinkStatusScreen +import app.aaps.pump.common.compose.RileyLinkStatusViewModel +import app.aaps.pump.medtronic.R + +class MedtronicComposeContent( + private val pluginName: String, + private val blePreCheck: BlePreCheck +) : ComposablePluginContent { + + @Composable + override fun Render( + setToolbarConfig: (ToolbarConfig) -> Unit, + onNavigateBack: () -> Unit, + onSettings: (() -> Unit)? + ) { + val overviewViewModel: MedtronicOverviewViewModel = hiltViewModel() + val snackbarHostState = LocalSnackbarHostState.current + + // Navigation state + var showRileyLinkPairWizard by remember { mutableStateOf(false) } + var showHistory by remember { mutableStateOf(false) } + var showRileyLinkStats by remember { mutableStateOf(false) } + + // Dialog state + var showDialog by remember { mutableStateOf(false) } + var dialogTitle by remember { mutableStateOf("") } + var dialogMessage by remember { mutableStateOf("") } + + // Toolbar + val overviewNavIcon: @Composable () -> Unit = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(app.aaps.core.ui.R.string.back)) + } + } + val settingsAction: @Composable RowScope.() -> Unit = { + onSettings?.let { action -> + IconButton(onClick = action) { + Icon(Icons.Filled.Settings, contentDescription = stringResource(app.aaps.core.ui.R.string.settings)) + } + } + } + + val historyTitle = stringResource(app.aaps.core.ui.R.string.pump_history) + val subScreenNavIcon: @Composable () -> Unit = { + IconButton(onClick = { showHistory = false }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(app.aaps.core.ui.R.string.back)) + } + } + + val rlStatsTitle = stringResource(app.aaps.pump.common.hw.rileylink.R.string.rileylink_settings_tab1) + val rlStatsNavIcon: @Composable () -> Unit = { + IconButton(onClick = { showRileyLinkStats = false }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(app.aaps.core.ui.R.string.back)) + } + } + + LaunchedEffect(showRileyLinkPairWizard, showHistory, showRileyLinkStats) { + when { + showHistory -> setToolbarConfig(ToolbarConfig(title = historyTitle, navigationIcon = subScreenNavIcon, actions = {})) + showRileyLinkStats -> setToolbarConfig(ToolbarConfig(title = rlStatsTitle, navigationIcon = rlStatsNavIcon, actions = {})) + !showRileyLinkPairWizard -> setToolbarConfig(ToolbarConfig(title = pluginName, navigationIcon = overviewNavIcon, actions = settingsAction)) + } + } + + // Handle events + LaunchedEffect(overviewViewModel) { + overviewViewModel.events.collect { event -> + when (event) { + is MedtronicOverviewEvent.ShowHistory -> { + showHistory = true + } + + is MedtronicOverviewEvent.ShowRileyLinkPairWizard -> { + showRileyLinkPairWizard = true + } + + is MedtronicOverviewEvent.ShowRileyLinkStats -> { + showRileyLinkStats = true + } + + is MedtronicOverviewEvent.ShowDialog -> { + dialogTitle = event.title + dialogMessage = event.message + showDialog = true + } + + is MedtronicOverviewEvent.ShowSnackbar -> { + snackbarHostState.showSnackbar(event.message) + } + } + } + } + + // Dialog + if (showDialog) { + OkDialog(title = dialogTitle, message = dialogMessage, onDismiss = { showDialog = false }) + } + + // Content + when { + showRileyLinkPairWizard -> { + var bleCheckPassed by remember { mutableStateOf(false) } + + if (!bleCheckPassed) { + BlePreCheckHost( + blePreCheck = blePreCheck, + onReady = { bleCheckPassed = true }, + onFailed = { showRileyLinkPairWizard = false } + ) + } else { + val rlWizardViewModel: RileyLinkPairWizardViewModel = hiltViewModel() + + LaunchedEffect(rlWizardViewModel) { + rlWizardViewModel.events.collect { event -> + when (event) { + is RileyLinkPairWizardEvent.Finish -> + showRileyLinkPairWizard = false + } + } + } + + RileyLinkPairWizardScreen( + viewModel = rlWizardViewModel, + onFinish = { showRileyLinkPairWizard = false }, + onCancel = { showRileyLinkPairWizard = false } + ) + } + } + + showRileyLinkStats -> { + val rlStatusViewModel: RileyLinkStatusViewModel = hiltViewModel() + RileyLinkStatusScreen(viewModel = rlStatusViewModel) + } + + showHistory -> { + val historyViewModel: MedtronicHistoryViewModel = hiltViewModel() + val historyState by historyViewModel.uiState.collectAsStateWithLifecycle() + + MedtronicHistoryScreen( + state = historyState, + groups = historyViewModel.groups, + rh = historyViewModel.rh, + onSelectGroup = historyViewModel::selectGroup + ) + } + + else -> { + val uiState by overviewViewModel.uiState.collectAsStateWithLifecycle() + PumpOverviewScreen( + state = uiState, + customContent = { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Image( + painter = painterResource(R.drawable.ic_medtronic_veo), + contentDescription = null, + modifier = Modifier.height(100.dp), + contentScale = ContentScale.Fit + ) + } + } + ) + } + } + } +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryScreen.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryScreen.kt new file mode 100644 index 000000000000..db2280fa6c97 --- /dev/null +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryScreen.kt @@ -0,0 +1,139 @@ +package app.aaps.pump.medtronic.compose + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.ui.compose.AapsCard +import app.aaps.core.ui.compose.AapsSpacing +import app.aaps.core.ui.compose.LocalDateUtil +import app.aaps.pump.common.defs.PumpHistoryEntryGroup +import app.aaps.pump.medtronic.comm.history.pump.PumpHistoryEntry + +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun MedtronicHistoryScreen( + state: MedtronicHistoryUiState, + groups: List, + rh: ResourceHelper, + onSelectGroup: (PumpHistoryEntryGroup) -> Unit +) { + val dateUtil = LocalDateUtil.current + + val groupedByDay by remember(state.records) { + derivedStateOf { + state.records + .filter { it.atechDateTime != 0L } + .groupBy { dateUtil.dateString(it.atechDateTime) } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + // Filter chips + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = AapsSpacing.extraLarge, vertical = AapsSpacing.medium), + horizontalArrangement = Arrangement.spacedBy(AapsSpacing.medium) + ) { + groups.forEach { group -> + FilterChip( + selected = state.selectedGroup == group, + onClick = { onSelectGroup(group) }, + label = { Text(group.translated ?: "") } + ) + } + } + + // History list with day headers + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = AapsSpacing.extraLarge), + verticalArrangement = Arrangement.spacedBy(AapsSpacing.medium) + ) { + groupedByDay.forEach { (dateString, itemsForDay) -> + stickyHeader(key = "header_$dateString") { + Text( + text = dateUtil.dateStringRelative(itemsForDay.first().atechDateTime, rh), + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = AapsSpacing.medium), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + + items(itemsForDay) { record -> + MedtronicHistoryCard(record, dateUtil) + } + } + } + } +} + +@Composable +private fun MedtronicHistoryCard( + record: PumpHistoryEntry, + dateUtil: DateUtil +) { + AapsCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.padding(AapsSpacing.large), + ) { + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = record.entryType.description, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.width(AapsSpacing.medium)) + Text( + text = dateUtil.timeString(record.atechDateTime), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (record.displayableValue.isNotEmpty()) { + Text( + text = record.displayableValue, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = AapsSpacing.small) + ) + } + } + } + } +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryViewModel.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryViewModel.kt new file mode 100644 index 000000000000..30f401f11b67 --- /dev/null +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicHistoryViewModel.kt @@ -0,0 +1,46 @@ +package app.aaps.pump.medtronic.compose + +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.pump.common.defs.PumpHistoryEntryGroup +import app.aaps.pump.medtronic.comm.history.pump.PumpHistoryEntry +import app.aaps.pump.medtronic.data.MedtronicHistoryData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +data class MedtronicHistoryUiState( + val selectedGroup: PumpHistoryEntryGroup = PumpHistoryEntryGroup.All, + val records: List = emptyList() +) + +@Stable +@HiltViewModel +class MedtronicHistoryViewModel @Inject constructor( + val rh: ResourceHelper, + private val medtronicHistoryData: MedtronicHistoryData +) : ViewModel() { + + private val _uiState = MutableStateFlow(MedtronicHistoryUiState()) + val uiState: StateFlow = _uiState + + val groups: List = PumpHistoryEntryGroup.getTranslatedList(rh) + + init { + filterHistory(PumpHistoryEntryGroup.All) + } + + fun selectGroup(group: PumpHistoryEntryGroup) { + filterHistory(group) + } + + private fun filterHistory(group: PumpHistoryEntryGroup) { + val all = medtronicHistoryData.allHistory + val filtered = if (group == PumpHistoryEntryGroup.All) all + else all.filter { it.entryType.group == group } + _uiState.update { it.copy(selectedGroup = group, records = filtered) } + } +} 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 new file mode 100644 index 000000000000..ceae5e9b134b --- /dev/null +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/compose/MedtronicOverviewViewModel.kt @@ -0,0 +1,405 @@ +package app.aaps.pump.medtronic.compose + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Block +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material.icons.filled.SettingsInputAntenna +import androidx.compose.material.icons.filled.Timeline +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +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.bus.RxBus +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.ui.compose.StatusLevel +import app.aaps.core.ui.compose.pump.ActionCategory +import app.aaps.core.ui.compose.pump.PumpAction +import app.aaps.core.ui.compose.pump.PumpCommunicationStatus +import app.aaps.core.ui.compose.pump.PumpInfoRow +import app.aaps.core.ui.compose.pump.PumpOverviewUiState +import app.aaps.core.ui.compose.pump.tickerFlow +import app.aaps.pump.common.events.EventRileyLinkDeviceStatusChange +import app.aaps.pump.common.extensions.stringResource +import app.aaps.pump.common.hw.rileylink.defs.RileyLinkServiceState +import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice +import app.aaps.pump.common.hw.rileylink.service.RileyLinkServiceData +import app.aaps.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask +import app.aaps.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor +import app.aaps.pump.common.hw.rileylink.service.tasks.WakeAndTuneTask +import app.aaps.pump.medtronic.MedtronicPumpPlugin +import app.aaps.pump.medtronic.R +import app.aaps.pump.medtronic.defs.BatteryType +import app.aaps.pump.medtronic.defs.MedtronicCommandType +import app.aaps.pump.medtronic.driver.MedtronicPumpStatus +import app.aaps.pump.medtronic.events.EventMedtronicPumpConfigurationChanged +import app.aaps.pump.medtronic.events.EventMedtronicPumpValuesChanged +import app.aaps.pump.medtronic.util.MedtronicUtil +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.Locale +import javax.inject.Inject +import javax.inject.Provider +import app.aaps.pump.common.hw.rileylink.R as RileyLinkR + +sealed class MedtronicOverviewEvent { + data object ShowHistory : MedtronicOverviewEvent() + data object ShowRileyLinkPairWizard : MedtronicOverviewEvent() + data object ShowRileyLinkStats : MedtronicOverviewEvent() + data class ShowDialog(val title: String, val message: String) : MedtronicOverviewEvent() + data class ShowSnackbar(val message: String) : MedtronicOverviewEvent() +} + +@Stable +@HiltViewModel +class MedtronicOverviewViewModel @Inject constructor( + private val rh: ResourceHelper, + private val medtronicPumpPlugin: MedtronicPumpPlugin, + private val medtronicPumpStatus: MedtronicPumpStatus, + private val medtronicUtil: MedtronicUtil, + private val rileyLinkServiceData: RileyLinkServiceData, + private val serviceTaskExecutor: ServiceTaskExecutor, + private val commandQueue: CommandQueue, + private val rxBus: RxBus, + private val dateUtil: DateUtil, + private val aapsLogger: AAPSLogger, + private val resetRileyLinkConfigurationTaskProvider: Provider, + private val wakeAndTuneTaskProvider: Provider, + @ApplicationContext private val context: Context +) : ViewModel() { + + companion object { + + private const val PLACEHOLDER = "-" + } + + private val communicationStatus = PumpCommunicationStatus(rxBus, commandQueue, context, viewModelScope) + + private val _events = MutableSharedFlow(extraBufferCapacity = 5) + val events: SharedFlow = _events + + private val medtronicRefresh = MutableStateFlow(0L).also { flow -> + viewModelScope.launch { + rxBus.toFlow(EventMedtronicPumpValuesChanged::class.java) + .collect { flow.value = System.currentTimeMillis() } + } + viewModelScope.launch { + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + .collect { flow.value = System.currentTimeMillis() } + } + viewModelScope.launch { + rxBus.toFlow(EventMedtronicPumpConfigurationChanged::class.java) + .collect { + aapsLogger.debug(LTag.PUMP, "EventMedtronicPumpConfigurationChanged triggered") + medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() + flow.value = System.currentTimeMillis() + } + } + } + + val uiState: StateFlow = combine( + communicationStatus.refreshTrigger, + medtronicRefresh, + tickerFlow(60_000L) + ) { _, _, _ -> buildUiState() }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = buildUiState() + ) + + private fun buildUiState(): PumpOverviewUiState { + return PumpOverviewUiState( + statusBanner = communicationStatus.statusBanner(), + infoRows = buildInfoRows(), + primaryActions = buildPrimaryActions(), + managementActions = buildManagementActions(), + queueStatus = communicationStatus.queueStatus() + ) + } + + // region Info Rows + + private fun buildInfoRows(): List = buildList { + // RileyLink status + val rlState = rileyLinkServiceData.rileyLinkServiceState + val rlError = rileyLinkServiceData.rileyLinkError + val rlStatusText = when { + rlState == RileyLinkServiceState.NotStarted -> rh.gs(rlState.resourceId) + rlState.isError() && rlError != null -> rh.gs(rlError.getResourceId(RileyLinkTargetDevice.MedtronicPump)) + else -> rh.gs(rlState.resourceId) + } + val rlLevel = if (rlState.isError() || rlError != null) StatusLevel.CRITICAL else StatusLevel.NORMAL + add(PumpInfoRow(label = rh.gs(RileyLinkR.string.rileylink_status), value = rlStatusText, level = rlLevel)) + + // RileyLink battery (conditional) + if (rileyLinkServiceData.showBatteryLevel) { + val batteryText = rileyLinkServiceData.batteryLevel?.let { "$it%" } ?: "?" + add(PumpInfoRow(label = rh.gs(R.string.rl_battery_label), value = batteryText)) + } + + // Pump status + val pumpStatusText = buildPumpStatusText() + add(PumpInfoRow(label = rh.gs(RileyLinkR.string.medtronic_pump_status), value = pumpStatusText)) + + // Last connection + val (lastConnText, lastConnLevel) = buildLastConnection() + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.last_connection_label), value = lastConnText, level = lastConnLevel)) + + // Last bolus + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.last_bolus_label), value = buildLastBolus())) + + // Base basal rate + val basalText = "(" + medtronicPumpStatus.activeProfileName + ") " + + rh.gs(app.aaps.core.ui.R.string.pump_base_basal_rate, medtronicPumpPlugin.baseBasalRate.cU) + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.base_basal_rate_label), value = basalText)) + + // Temp basal + val tbrText = buildTempBasal() + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.tempbasal_label), value = tbrText, visible = tbrText.isNotEmpty())) + + // Battery + val (batteryText, batteryLevel) = buildBattery() + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.battery_label), value = batteryText, level = batteryLevel)) + + // Reservoir + val (reservoirText, reservoirLevel) = buildReservoir() + add(PumpInfoRow(label = rh.gs(app.aaps.core.ui.R.string.reservoir_label), value = reservoirText, level = reservoirLevel)) + + // Errors + val errorsText = medtronicPumpStatus.errorInfo + val errorsLevel = if (errorsText != PLACEHOLDER) StatusLevel.CRITICAL else StatusLevel.NORMAL + add(PumpInfoRow(label = rh.gs(R.string.medtronic_errors), value = errorsText, level = errorsLevel)) + } + + private fun buildPumpStatusText(): String { + return when (medtronicPumpStatus.pumpDeviceState) { + app.aaps.core.interfaces.pump.defs.PumpDeviceState.Sleeping -> + rh.gs(medtronicPumpStatus.pumpDeviceState.stringResource()) + + app.aaps.core.interfaces.pump.defs.PumpDeviceState.NeverContacted, + app.aaps.core.interfaces.pump.defs.PumpDeviceState.WakingUp, + app.aaps.core.interfaces.pump.defs.PumpDeviceState.PumpUnreachable, + app.aaps.core.interfaces.pump.defs.PumpDeviceState.ErrorWhenCommunicating, + app.aaps.core.interfaces.pump.defs.PumpDeviceState.TimeoutWhenCommunicating, + app.aaps.core.interfaces.pump.defs.PumpDeviceState.InvalidConfiguration -> + rh.gs(medtronicPumpStatus.pumpDeviceState.stringResource()) + + app.aaps.core.interfaces.pump.defs.PumpDeviceState.Active -> { + val cmd = medtronicUtil.getCurrentCommand() + if (cmd == null) { + rh.gs(medtronicPumpStatus.pumpDeviceState.stringResource()) + } else { + val cmdResourceId = cmd.resourceId + if (cmd == MedtronicCommandType.GetHistoryData) { + medtronicUtil.frameNumber?.let { + rh.gs(cmdResourceId!!, medtronicUtil.pageNumber, medtronicUtil.frameNumber) + } ?: rh.gs(R.string.medtronic_cmd_desc_get_history_request, medtronicUtil.pageNumber) + } else { + cmdResourceId?.let { rh.gs(it) } ?: cmd.commandDescription + } + } + } + } + } + + private fun buildLastConnection(): Pair { + val lastConnection = medtronicPumpStatus.lastConnection + if (lastConnection == 0L) return PLACEHOLDER to StatusLevel.NORMAL + + val min = (System.currentTimeMillis() - lastConnection) / 1000 / 60 + return when { + lastConnection + 60 * 1000 > System.currentTimeMillis() -> + rh.gs(R.string.medtronic_pump_connected_now) to StatusLevel.NORMAL + + lastConnection + 30 * 60 * 1000 < System.currentTimeMillis() -> { + val text = when { + min < 60 -> rh.gs(app.aaps.core.interfaces.R.string.minago, min) + + min < 1440 -> { + val h = (min / 60).toInt() + rh.gq(RileyLinkR.plurals.duration_hours, h, h) + " " + rh.gs(R.string.ago) + } + + else -> { + val d = (min / 60 / 24).toInt() + rh.gq(RileyLinkR.plurals.duration_days, d, d) + " " + rh.gs(R.string.ago) + } + } + text to StatusLevel.WARNING + } + + else -> + dateUtil.minAgo(rh, lastConnection) to StatusLevel.NORMAL + } + } + + private fun buildLastBolus(): String { + val bolus = medtronicPumpStatus.lastBolusAmount + val bolusTime = medtronicPumpStatus.lastBolusTime + if (bolus == null || bolusTime == null) return "" + + val agoMsc = System.currentTimeMillis() - bolusTime.time + val bolusMinAgo = agoMsc.toDouble() / 60.0 / 1000.0 + val unit = rh.gs(app.aaps.core.ui.R.string.insulin_unit_shortname) + val ago = when { + agoMsc < 60 * 1000 -> rh.gs(R.string.medtronic_pump_connected_now) + bolusMinAgo < 60 -> dateUtil.minAgo(rh, bolusTime.time) + else -> dateUtil.hourAgo(bolusTime.time, rh) + } + return rh.gs(R.string.mdt_last_bolus, bolus, unit, ago) + } + + private fun buildTempBasal(): String { + val tbrRemainingTime = medtronicPumpStatus.tbrRemainingTime ?: return "" + return rh.gs(R.string.mdt_tbr_remaining, medtronicPumpStatus.tempBasalAmount, tbrRemainingTime) + } + + private fun buildBattery(): Pair { + val remaining = medtronicPumpStatus.batteryRemaining + val text = if (medtronicPumpStatus.batteryType == BatteryType.None || medtronicPumpStatus.batteryVoltage == null) { + remaining?.let { "$it%" } ?: rh.gs(app.aaps.core.ui.R.string.unknown) + } else { + (remaining?.let { "$it% " } ?: "") + + String.format(Locale.getDefault(), "(%.2f V)", medtronicPumpStatus.batteryVoltage) + } + val level = when { + remaining == null -> StatusLevel.NORMAL + remaining <= 10 -> StatusLevel.CRITICAL + remaining <= 25 -> StatusLevel.WARNING + else -> StatusLevel.NORMAL + } + return text to level + } + + private fun buildReservoir(): Pair { + val remaining = medtronicPumpStatus.reservoirRemainingUnits + val full = medtronicPumpStatus.reservoirFullUnits + val text = rh.gs(app.aaps.core.ui.R.string.reservoir_value, remaining, full) + val level = when { + remaining <= 20.0 -> StatusLevel.CRITICAL + remaining <= 50.0 -> StatusLevel.WARNING + else -> StatusLevel.NORMAL + } + return text to level + } + + // endregion + + // region Actions + + private fun buildPrimaryActions(): List { + return listOf( + PumpAction( + label = rh.gs(app.aaps.core.ui.R.string.refresh), + icon = Icons.Filled.Refresh, + onClick = { onRefreshClicked() } + ) + ) + } + + private fun buildManagementActions(): List { + val isConfigured = medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() == true + + return listOf( + PumpAction( + label = rh.gs(RileyLinkR.string.rileylink_pair), + icon = Icons.Filled.Bluetooth, + category = ActionCategory.MANAGEMENT, + onClick = { _events.tryEmit(MedtronicOverviewEvent.ShowRileyLinkPairWizard) } + ), + PumpAction( + label = rh.gs(app.aaps.core.ui.R.string.pump_history), + icon = Icons.Filled.History, + category = ActionCategory.MANAGEMENT, + onClick = { _events.tryEmit(MedtronicOverviewEvent.ShowHistory) } + ), + PumpAction( + label = rh.gs(R.string.riley_statistics), + icon = Icons.Filled.Timeline, + category = ActionCategory.MANAGEMENT, + onClick = { + if (isConfigured) { + _events.tryEmit(MedtronicOverviewEvent.ShowRileyLinkStats) + } else { + emitNotConfiguredDialog() + } + } + ), + PumpAction( + label = rh.gs(R.string.medtronic_custom_action_wake_and_tune), + icon = Icons.Filled.SettingsInputAntenna, + category = ActionCategory.MANAGEMENT, + onClick = { + if (isConfigured) { + serviceTaskExecutor.startTask(wakeAndTuneTaskProvider.get()) + _events.tryEmit(MedtronicOverviewEvent.ShowSnackbar(rh.gs(R.string.medtronic_custom_action_wake_and_tune))) + } else { + emitNotConfiguredDialog() + } + } + ), + PumpAction( + label = rh.gs(R.string.medtronic_custom_action_clear_bolus_block), + icon = Icons.Filled.Block, + category = ActionCategory.MANAGEMENT, + visible = medtronicPumpPlugin.isBusyBlockingEnabled(), + onClick = { + medtronicPumpPlugin.clearBusyTimestamps() + _events.tryEmit(MedtronicOverviewEvent.ShowSnackbar(rh.gs(R.string.medtronic_custom_action_clear_bolus_block))) + } + ), + PumpAction( + label = rh.gs(R.string.medtronic_custom_action_reset_rileylink), + icon = Icons.Filled.RestartAlt, + category = ActionCategory.MANAGEMENT, + onClick = { + serviceTaskExecutor.startTask(resetRileyLinkConfigurationTaskProvider.get()) + _events.tryEmit(MedtronicOverviewEvent.ShowSnackbar(rh.gs(RileyLinkR.string.rileylink_config_reset))) + } + ) + ) + } + + // endregion + + // region Action handlers + + private fun onRefreshClicked() { + if (medtronicPumpPlugin.rileyLinkService?.verifyConfiguration() != true) { + emitNotConfiguredDialog() + return + } + medtronicPumpPlugin.resetStatusState() + commandQueue.readStatus(rh.gs(R.string.clicked_refresh), object : Callback() { + override fun run() { /* refresh button re-enabled via EventRefreshButtonState */ + } + }) + } + + private fun emitNotConfiguredDialog() { + _events.tryEmit( + MedtronicOverviewEvent.ShowDialog( + rh.gs(R.string.medtronic_warning), + rh.gs(R.string.medtronic_error_operation_not_possible_no_configuration) + ) + ) + } + + // endregion + +} diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/di/MedtronicModule.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/di/MedtronicModule.kt index 566661bedf7f..089bfe2c6189 100644 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/di/MedtronicModule.kt +++ b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/di/MedtronicModule.kt @@ -2,12 +2,10 @@ package app.aaps.pump.medtronic.di import app.aaps.core.interfaces.di.PumpDriver import app.aaps.core.interfaces.plugin.PluginBase -import app.aaps.pump.medtronic.MedtronicFragment import app.aaps.pump.medtronic.MedtronicPumpPlugin import app.aaps.pump.medtronic.comm.MedtronicCommunicationManager import app.aaps.pump.medtronic.comm.ui.MedtronicUIComm import app.aaps.pump.medtronic.comm.ui.MedtronicUITask -import app.aaps.pump.medtronic.dialog.MedtronicHistoryActivity import app.aaps.pump.medtronic.service.RileyLinkMedtronicService import dagger.Binds import dagger.Module @@ -22,10 +20,6 @@ import dagger.multibindings.IntoMap @Suppress("unused") abstract class MedtronicModule { - @ContributesAndroidInjector - abstract fun contributesMedtronicHistoryActivity(): MedtronicHistoryActivity - @ContributesAndroidInjector abstract fun contributesMedtronicFragment(): MedtronicFragment - @ContributesAndroidInjector abstract fun contributesRileyLinkMedtronicService(): RileyLinkMedtronicService diff --git a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/dialog/MedtronicHistoryActivity.kt b/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/dialog/MedtronicHistoryActivity.kt deleted file mode 100644 index 6744166afb39..000000000000 --- a/pump/medtronic/src/main/kotlin/app/aaps/pump/medtronic/dialog/MedtronicHistoryActivity.kt +++ /dev/null @@ -1,184 +0,0 @@ -package app.aaps.pump.medtronic.dialog - -import android.os.Bundle -import android.os.SystemClock -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.pump.common.defs.PumpHistoryEntryGroup -import app.aaps.pump.medtronic.R -import app.aaps.pump.medtronic.comm.history.pump.PumpHistoryEntry -import app.aaps.pump.medtronic.data.MedtronicHistoryData -import dagger.android.DaggerActivity -import javax.inject.Inject - -class MedtronicHistoryActivity : DaggerActivity() { - - @Inject lateinit var medtronicHistoryData: MedtronicHistoryData - @Inject lateinit var rh: ResourceHelper - - lateinit var historyTypeSpinner: Spinner - lateinit var statusView: TextView - lateinit var recyclerView: RecyclerView - lateinit var llm: LinearLayoutManager - lateinit var recyclerViewAdapter: RecyclerViewAdapter - - var filteredHistoryList: MutableList = ArrayList() - var manualChange = false - lateinit var typeListFull: List - - //private var _binding: MedtronicHistoryActivityBinding? = null - - //@Inject - //var fragmentInjector: DispatchingAndroidInjector? = null - - // This property is only valid between onCreateView and - // onDestroyView. - //private val binding get() = _binding!! - - private fun filterHistory(group: PumpHistoryEntryGroup) { - filteredHistoryList.clear() - val list: MutableList = ArrayList() - list.addAll(medtronicHistoryData.allHistory) - - //LOG.debug("Items on full list: {}", list.size()); - if (group === PumpHistoryEntryGroup.All) { - filteredHistoryList.addAll(list) - } else { - for (pumpHistoryEntry in list) { - if (pumpHistoryEntry.entryType.group === group) { - filteredHistoryList.add(pumpHistoryEntry) - } - } - } - - recyclerViewAdapter.setHistoryListInternal(filteredHistoryList) - recyclerViewAdapter.notifyDataSetChanged() - - //LOG.debug("Items on filtered list: {}", filteredHistoryList.size()); - } - - override fun onResume() { - super.onResume() - filterHistory(selectedGroup) - setHistoryTypeSpinner() - } - - private fun setHistoryTypeSpinner() { - manualChange = true - for (i in typeListFull.indices) { - if (typeListFull[i].entryGroup === selectedGroup) { - historyTypeSpinner.setSelection(i) - break - } - } - SystemClock.sleep(200) - manualChange = false - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.medtronic_history_activity) - historyTypeSpinner = findViewById(R.id.medtronic_historytype) - statusView = findViewById(R.id.medtronic_historystatus) - recyclerView = findViewById(R.id.medtronic_history_recyclerview) - recyclerView.setHasFixedSize(true) - llm = LinearLayoutManager(this) - recyclerView.layoutManager = llm - recyclerViewAdapter = RecyclerViewAdapter(filteredHistoryList) - recyclerView.adapter = recyclerViewAdapter - statusView.visibility = View.GONE - typeListFull = getTypeList(PumpHistoryEntryGroup.getTranslatedList(rh)) - val spinnerAdapter = ArrayAdapter(this, app.aaps.core.ui.R.layout.spinner_centered, typeListFull) - historyTypeSpinner.adapter = spinnerAdapter - historyTypeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - if (manualChange) return - val selected = historyTypeSpinner.selectedItem as TypeList - showingType = selected - selectedGroup = selected.entryGroup - filterHistory(selectedGroup) - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - if (manualChange) return - filterHistory(PumpHistoryEntryGroup.All) - } - } - } - - override fun onDestroy() { - super.onDestroy() - recyclerView.adapter = null - historyTypeSpinner.adapter = null - historyTypeSpinner.onItemSelectedListener = null - } - - private fun getTypeList(list: List): List { - val typeList = ArrayList() - for (pumpHistoryEntryGroup in list) { - typeList.add(TypeList(pumpHistoryEntryGroup)) - } - return typeList - } - - class TypeList internal constructor(var entryGroup: PumpHistoryEntryGroup) { - - var name: String = entryGroup.translated!! - override fun toString(): String { - return name - } - } - - class RecyclerViewAdapter internal constructor(var historyList: List) : RecyclerView.Adapter() { - - fun setHistoryListInternal(historyList: List) { - // this.historyList.clear(); - // this.historyList.addAll(historyList); - this.historyList = historyList - - // this.notifyDataSetChanged(); - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): HistoryViewHolder { - val v = LayoutInflater.from(viewGroup.context).inflate( - R.layout.medtronic_history_item, // - viewGroup, false - ) - return HistoryViewHolder(v) - } - - override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { - val record = historyList[position] - holder.timeView.text = record.dateTimeString - holder.typeView.text = record.entryType.description - holder.valueView.text = record.displayableValue - } - - override fun getItemCount(): Int { - return historyList.size - } - - class HistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - // cv = (CardView)itemView.findViewById(R.id.rileylink_history_item); - var timeView: TextView = itemView.findViewById(R.id.medtronic_history_time) - var typeView: TextView = itemView.findViewById(R.id.medtronic_history_source) - var valueView: TextView = itemView.findViewById(R.id.medtronic_history_description) - } - - } - - companion object { - - var showingType: TypeList? = null - var selectedGroup = PumpHistoryEntryGroup.All - } -} \ No newline at end of file diff --git a/pump/medtronic/src/main/res/layout/medtronic_fragment.xml b/pump/medtronic/src/main/res/layout/medtronic_fragment.xml deleted file mode 100644 index 437cdf3455c8..000000000000 --- a/pump/medtronic/src/main/res/layout/medtronic_fragment.xml +++ /dev/null @@ -1,596 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pump/medtronic/src/main/res/layout/medtronic_history_activity.xml b/pump/medtronic/src/main/res/layout/medtronic_history_activity.xml deleted file mode 100644 index 961b4bf96c42..000000000000 --- a/pump/medtronic/src/main/res/layout/medtronic_history_activity.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/pump/medtronic/src/main/res/layout/medtronic_history_item.xml b/pump/medtronic/src/main/res/layout/medtronic_history_item.xml deleted file mode 100644 index ec964cdfd33f..000000000000 --- a/pump/medtronic/src/main/res/layout/medtronic_history_item.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - diff --git a/pump/medtronic/src/main/res/values/strings.xml b/pump/medtronic/src/main/res/values/strings.xml index 5a3fb2930e22..71458c33dbb5 100644 --- a/pump/medtronic/src/main/res/values/strings.xml +++ b/pump/medtronic/src/main/res/values/strings.xml @@ -115,7 +115,6 @@ ^\\d{6} Invalid pump history data detected. Open new issue and provide logs. RL Stats - Type: Clicked refresh Scheduled Status Refresh diff --git a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/ui/wizard/compose/OmnipodOverviewEvent.kt b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/ui/wizard/compose/OmnipodOverviewEvent.kt index 6e2c4446a2d4..0657dc5482fe 100644 --- a/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/ui/wizard/compose/OmnipodOverviewEvent.kt +++ b/pump/omnipod/common/src/main/kotlin/app/aaps/pump/omnipod/common/ui/wizard/compose/OmnipodOverviewEvent.kt @@ -14,5 +14,6 @@ sealed class OmnipodOverviewEvent { data class ShowErrorDialog(val title: String, val message: String) : OmnipodOverviewEvent() data class StartActivity(val intent: Intent) : OmnipodOverviewEvent() data object ShowRileyLinkPairWizard : OmnipodOverviewEvent() + data object ShowRileyLinkStats : OmnipodOverviewEvent() data class ShowSnackbar(val message: String) : OmnipodOverviewEvent() } diff --git a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/OmnipodDashComposeContent.kt b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/OmnipodDashComposeContent.kt index d8c9dbcbe26d..ce665ef4df79 100644 --- a/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/OmnipodDashComposeContent.kt +++ b/pump/omnipod/dash/src/main/kotlin/app/aaps/pump/omnipod/dash/ui/compose/OmnipodDashComposeContent.kt @@ -125,6 +125,7 @@ class OmnipodDashComposeContent( } is OmnipodOverviewEvent.ShowRileyLinkPairWizard -> Unit // Not applicable for Dash + is OmnipodOverviewEvent.ShowRileyLinkStats -> Unit // Not applicable for Dash is OmnipodOverviewEvent.ShowSnackbar -> Unit // Not used by Dash } } 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 07911b975e5f..de86f35d074b 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 @@ -1,7 +1,6 @@ package app.aaps.pump.omnipod.eros.ui.compose import android.content.Context -import android.content.Intent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Delete @@ -43,7 +42,6 @@ import app.aaps.core.ui.compose.pump.tickerFlow import app.aaps.pump.common.events.EventRileyLinkDeviceStatusChange import app.aaps.pump.common.hw.rileylink.defs.RileyLinkServiceState import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice -import app.aaps.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity import app.aaps.pump.common.hw.rileylink.service.RileyLinkServiceData import app.aaps.pump.common.hw.rileylink.service.tasks.ResetRileyLinkConfigurationTask import app.aaps.pump.common.hw.rileylink.service.tasks.ServiceTaskExecutor @@ -353,7 +351,7 @@ class ErosOverviewViewModel @Inject constructor( visible = omnipodManager.isRileylinkStatsButtonEnabled, onClick = { if (omnipodErosPumpPlugin.rileyLinkService?.verifyConfiguration() == true) { - _events.tryEmit(OmnipodOverviewEvent.StartActivity(Intent(context, RileyLinkStatusActivity::class.java))) + _events.tryEmit(OmnipodOverviewEvent.ShowRileyLinkStats) } } ), diff --git a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/OmnipodErosComposeContent.kt b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/OmnipodErosComposeContent.kt index b8f6d452a018..2e3976d59ebd 100644 --- a/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/OmnipodErosComposeContent.kt +++ b/pump/omnipod/eros/src/main/java/app/aaps/pump/omnipod/eros/ui/compose/OmnipodErosComposeContent.kt @@ -29,6 +29,8 @@ import app.aaps.core.ui.compose.pump.PumpOverviewScreen import app.aaps.pump.common.compose.RileyLinkPairWizardEvent import app.aaps.pump.common.compose.RileyLinkPairWizardScreen import app.aaps.pump.common.compose.RileyLinkPairWizardViewModel +import app.aaps.pump.common.compose.RileyLinkStatusScreen +import app.aaps.pump.common.compose.RileyLinkStatusViewModel import app.aaps.pump.omnipod.common.ui.compose.PodImage import app.aaps.pump.omnipod.common.ui.wizard.compose.ActivationType import app.aaps.pump.omnipod.common.ui.wizard.compose.OmnipodOverviewEvent @@ -55,6 +57,7 @@ class OmnipodErosComposeContent( var showWizard by remember { mutableStateOf(false) } var showHistory by remember { mutableStateOf(false) } var showRileyLinkPairWizard by remember { mutableStateOf(false) } + var showRileyLinkStats by remember { mutableStateOf(false) } var wizardActivationType by remember { mutableStateOf(null) } var isDeactivation by remember { mutableStateOf(false) } @@ -82,9 +85,20 @@ class OmnipodErosComposeContent( } // Restore overview toolbar when not in wizard - LaunchedEffect(showWizard, showRileyLinkPairWizard) { - if (!showWizard && !showRileyLinkPairWizard) { + LaunchedEffect(showWizard, showRileyLinkPairWizard, showRileyLinkStats) { + if (!showWizard && !showRileyLinkPairWizard && !showRileyLinkStats) { setToolbarConfig(ToolbarConfig(title = pluginName, navigationIcon = overviewNavIcon, actions = settingsAction)) + } else if (showRileyLinkStats) { + setToolbarConfig( + ToolbarConfig( + title = context.getString(app.aaps.pump.common.hw.rileylink.R.string.rileylink_settings_tab1), + navigationIcon = { + IconButton(onClick = { showRileyLinkStats = false }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(app.aaps.core.ui.R.string.back)) + } + }, + actions = {} + )) } } @@ -133,6 +147,10 @@ class OmnipodErosComposeContent( showRileyLinkPairWizard = true } + is OmnipodOverviewEvent.ShowRileyLinkStats -> { + showRileyLinkStats = true + } + is OmnipodOverviewEvent.ShowSnackbar -> { snackbarHostState.showSnackbar(event.message) } @@ -162,27 +180,37 @@ class OmnipodErosComposeContent( // Content: overview, wizard, history, or RL pair when { showRileyLinkPairWizard -> { - BlePreCheckHost( - blePreCheck = blePreCheck, - onFailed = { showRileyLinkPairWizard = false } - ) + var bleCheckPassed by remember { mutableStateOf(false) } - val rlWizardViewModel: RileyLinkPairWizardViewModel = hiltViewModel() + if (!bleCheckPassed) { + BlePreCheckHost( + blePreCheck = blePreCheck, + onReady = { bleCheckPassed = true }, + onFailed = { showRileyLinkPairWizard = false } + ) + } else { + val rlWizardViewModel: RileyLinkPairWizardViewModel = hiltViewModel() - LaunchedEffect(rlWizardViewModel) { - rlWizardViewModel.events.collect { event -> - when (event) { - is RileyLinkPairWizardEvent.Finish -> - showRileyLinkPairWizard = false + LaunchedEffect(rlWizardViewModel) { + rlWizardViewModel.events.collect { event -> + when (event) { + is RileyLinkPairWizardEvent.Finish -> + showRileyLinkPairWizard = false + } } } + + RileyLinkPairWizardScreen( + viewModel = rlWizardViewModel, + onFinish = { showRileyLinkPairWizard = false }, + onCancel = { showRileyLinkPairWizard = false } + ) } + } - RileyLinkPairWizardScreen( - viewModel = rlWizardViewModel, - onFinish = { showRileyLinkPairWizard = false }, - onCancel = { showRileyLinkPairWizard = false } - ) + showRileyLinkStats -> { + val rlStatusViewModel: RileyLinkStatusViewModel = hiltViewModel() + RileyLinkStatusScreen(viewModel = rlStatusViewModel) } showWizard -> { 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 4c01744d5d53..ab2d9b8d56e1 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 @@ -1,6 +1,7 @@ package app.aaps.pump.omnipod.eros import app.aaps.core.interfaces.protection.ProtectionCheck +import app.aaps.core.interfaces.pump.BlePreCheck import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.ui.UiInteraction @@ -35,6 +36,7 @@ class OmnipodErosPumpPluginTest : TestBaseWithProfile() { @Mock lateinit var aapsOmnipodUtil: AapsOmnipodUtil @Mock lateinit var omnipodAlertUtil: OmnipodAlertUtil @Mock lateinit var protectionCheck: ProtectionCheck + @Mock lateinit var blePreCheck: BlePreCheck private lateinit var plugin: OmnipodErosPumpPlugin @@ -48,7 +50,7 @@ class OmnipodErosPumpPluginTest : TestBaseWithProfile() { aapsLogger, rh, preferences, commandQueue, TestAapsSchedulers(), rxBus, context, erosPodStateManager, aapsOmnipodErosManager, fabricPrivacy, rileyLinkServiceData, aapsOmnipodUtil, rileyLinkUtil, omnipodAlertUtil, pumpSync, uiInteraction, notificationManager, erosHistoryDatabase, pumpEnactResultProvider, - protectionCheck + protectionCheck, blePreCheck ) } diff --git a/pump/rileylink/src/main/AndroidManifest.xml b/pump/rileylink/src/main/AndroidManifest.xml index a5be29d73470..2919589e18f1 100644 --- a/pump/rileylink/src/main/AndroidManifest.xml +++ b/pump/rileylink/src/main/AndroidManifest.xml @@ -4,22 +4,6 @@ - - - - - - - - - - + diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardScreen.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardScreen.kt index 5acf952abd73..0ae993f2c09a 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardScreen.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkPairWizardScreen.kt @@ -17,6 +17,7 @@ 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.compose.AapsSpacing import app.aaps.core.ui.compose.pump.BleScanStep import app.aaps.core.ui.compose.pump.WizardButton import app.aaps.core.ui.compose.pump.WizardScreen @@ -71,7 +72,7 @@ fun RileyLinkPairWizardScreen( .fillMaxWidth() .padding(vertical = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(AapsSpacing.extraLarge) ) { Icon( imageVector = Icons.Filled.CheckCircle, diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusScreen.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusScreen.kt new file mode 100644 index 000000000000..2ef7cc187814 --- /dev/null +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusScreen.kt @@ -0,0 +1,173 @@ +package app.aaps.pump.common.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.aaps.core.ui.compose.AapsCard +import app.aaps.core.ui.compose.AapsSpacing +import app.aaps.pump.common.hw.rileylink.R +import kotlinx.coroutines.launch + +@Composable +fun RileyLinkStatusScreen( + viewModel: RileyLinkStatusViewModel +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val pagerState = rememberPagerState(pageCount = { 2 }) + val scope = rememberCoroutineScope() + + Column(modifier = Modifier.fillMaxSize()) { + TabRow(selectedTabIndex = pagerState.currentPage) { + Tab( + selected = pagerState.currentPage == 0, + onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(stringResource(R.string.rileylink_settings_tab1)) } + ) + Tab( + selected = pagerState.currentPage == 1, + onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(stringResource(R.string.rileylink_settings_tab2)) } + ) + } + + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> + when (page) { + 0 -> GeneralTab(state) + 1 -> HistoryTab(state) + } + } + } +} + +@Composable +private fun GeneralTab(state: RileyLinkStatusUiState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(AapsSpacing.extraLarge), + verticalArrangement = Arrangement.spacedBy(AapsSpacing.large) + ) { + // RileyLink card + AapsCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(AapsSpacing.large), verticalArrangement = Arrangement.spacedBy(AapsSpacing.small)) { + Text( + text = stringResource(R.string.rileylink_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + HorizontalDivider() + InfoRow(stringResource(R.string.rileylink_address), state.address) + InfoRow(stringResource(R.string.rileylink_name), state.name) + state.batteryLevel?.let { InfoRow(stringResource(R.string.rileylink_battery_level), it) } + InfoRow(stringResource(R.string.rileylink_connection_status), state.connectionStatus) + InfoRow(stringResource(R.string.rileylink_connection_error), state.connectionError) + InfoRow(stringResource(R.string.rileylink_firmware_version), state.firmwareVersion) + } + } + + // Device card + AapsCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(AapsSpacing.large), verticalArrangement = Arrangement.spacedBy(AapsSpacing.small)) { + Text( + text = stringResource(R.string.rileylink_device), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + HorizontalDivider() + InfoRow(stringResource(R.string.rileylink_device_type), state.deviceType) + state.configuredDeviceModel?.let { InfoRow(stringResource(R.string.rileylink_configured_device_model), it) } + state.connectedDeviceModel?.let { InfoRow(stringResource(R.string.rileylink_connected_device_model), it) } + InfoRow(stringResource(R.string.rileylink_pump_serial_number), state.serialNumber) + InfoRow(stringResource(R.string.rileylink_pump_frequency), state.pumpFrequency) + state.lastUsedFrequency?.let { InfoRow(stringResource(R.string.rileylink_last_used_frequency), it) } + InfoRow(stringResource(R.string.rileylink_last_device_contact), state.lastDeviceContact) + } + } + } +} + +@Composable +private fun HistoryTab(state: RileyLinkStatusUiState) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(AapsSpacing.extraLarge), + verticalArrangement = Arrangement.spacedBy(AapsSpacing.medium) + ) { + items(state.historyItems) { item -> + AapsCard(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.padding(AapsSpacing.large)) { + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = item.source, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.width(AapsSpacing.medium)) + Text( + text = item.time, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = item.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = AapsSpacing.small) + ) + } + } + } + } + } +} + +@Composable +private fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1.5f) + ) + } +} 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 new file mode 100644 index 000000000000..324fbe99078c --- /dev/null +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/compose/RileyLinkStatusViewModel.kt @@ -0,0 +1,151 @@ +package app.aaps.pump.common.compose + +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.pump.defs.PumpDeviceState +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.keys.interfaces.Preferences +import app.aaps.pump.common.events.EventRileyLinkDeviceStatusChange +import app.aaps.pump.common.hw.rileylink.R +import app.aaps.pump.common.hw.rileylink.RileyLinkUtil +import app.aaps.pump.common.hw.rileylink.data.RLHistoryItem +import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpDevice +import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice +import app.aaps.pump.common.hw.rileylink.keys.RileylinkBooleanPreferenceKey +import app.aaps.pump.common.hw.rileylink.service.RileyLinkServiceData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class RileyLinkStatusUiState( + // General + val address: String = "-", + val name: String = "-", + val batteryLevel: String? = null, + val connectionStatus: String = "-", + val connectionError: String = "-", + val firmwareVersion: String = "-", + val deviceType: String = "-", + val configuredDeviceModel: String? = null, + val connectedDeviceModel: String? = null, + val serialNumber: String = "-", + val pumpFrequency: String = "-", + val lastUsedFrequency: String? = null, + val lastDeviceContact: String = "-", + // History + val historyItems: List = emptyList() +) + +data class RileyLinkHistoryItem( + val time: String, + val source: String, + val description: String +) + +@Stable +@HiltViewModel +class RileyLinkStatusViewModel @Inject constructor( + private val rh: ResourceHelper, + private val rileyLinkServiceData: RileyLinkServiceData, + private val rileyLinkUtil: RileyLinkUtil, + private val activePlugin: ActivePlugin, + private val dateUtil: DateUtil, + private val preferences: Preferences, + private val rxBus: RxBus +) : ViewModel() { + + private val _uiState = MutableStateFlow(RileyLinkStatusUiState()) + val uiState: StateFlow = _uiState + + init { + refresh() + viewModelScope.launch { + rxBus.toFlow(EventRileyLinkDeviceStatusChange::class.java) + .collect { refresh() } + } + } + + fun refresh() { + _uiState.update { buildState() } + } + + private fun buildState(): RileyLinkStatusUiState { + val targetDevice = rileyLinkServiceData.targetDevice + val rileyLinkPumpDevice = activePlugin.activePumpInternal as? RileyLinkPumpDevice + ?: return RileyLinkStatusUiState() + val pumpInfo = rileyLinkPumpDevice.pumpInfo + + // Battery + val batteryLevel = if (preferences.get(RileylinkBooleanPreferenceKey.ShowReportedBatteryLevel)) { + rileyLinkServiceData.batteryLevel?.let { rh.gs(R.string.rileylink_battery_level_value, it) } ?: EMPTY + } else null + + // Firmware + val firmwareVersion = if (rileyLinkServiceData.isOrange && rileyLinkServiceData.versionOrangeFirmware != null) { + rh.gs(R.string.rileylink_firmware_version_value_orange, rileyLinkServiceData.versionOrangeFirmware, rileyLinkServiceData.versionOrangeHardware ?: EMPTY) + } else { + rh.gs(R.string.rileylink_firmware_version_value, rileyLinkServiceData.versionBLE113 ?: EMPTY, rileyLinkServiceData.versionCC110 ?: EMPTY) + } + + // Connected device details (Medtronic only) + val isMedtronic = targetDevice == RileyLinkTargetDevice.MedtronicPump + val configuredDeviceModel = if (isMedtronic) activePlugin.activePumpInternal.pumpDescription.pumpType.description else null + val connectedDeviceModel = if (isMedtronic) pumpInfo.connectedDeviceModel else null + + // Last used frequency + val lastUsedFrequency = rileyLinkServiceData.lastGoodFrequency?.let { + rh.gs(R.string.rileylink_pump_frequency_value, it) + } + + // Last device contact + val lastContact = rileyLinkPumpDevice.lastConnectionTimeMillis + val lastDeviceContact = if (lastContact == 0L) rh.gs(R.string.riley_link_ble_config_connected_never) + else dateUtil.dateAndTimeAndSecondsString(lastContact) + + // History + val historyItems = rileyLinkUtil.rileyLinkHistory + .filter { isValidHistoryItem(it) } + .sortedWith(RLHistoryItem.Comparator()) + .map { item -> + RileyLinkHistoryItem( + time = dateUtil.dateAndTimeAndSecondsString(item.dateTime.toDateTime().millis), + source = item.source.desc, + description = item.getDescription(rh) + ) + } + + return RileyLinkStatusUiState( + address = rileyLinkServiceData.rileyLinkAddress ?: EMPTY, + name = rileyLinkServiceData.rileyLinkName ?: EMPTY, + batteryLevel = batteryLevel, + connectionStatus = rh.gs(rileyLinkServiceData.rileyLinkServiceState.resourceId), + connectionError = rileyLinkServiceData.rileyLinkError?.let { rh.gs(it.getResourceId(targetDevice)) } ?: EMPTY, + firmwareVersion = firmwareVersion, + deviceType = rh.gs(targetDevice.resourceId), + configuredDeviceModel = configuredDeviceModel, + connectedDeviceModel = connectedDeviceModel, + serialNumber = pumpInfo.connectedDeviceSerialNumber, + pumpFrequency = pumpInfo.pumpFrequency, + lastUsedFrequency = lastUsedFrequency, + lastDeviceContact = lastDeviceContact, + historyItems = historyItems + ) + } + + private fun isValidHistoryItem(item: RLHistoryItem): Boolean = + item.pumpDeviceState !== PumpDeviceState.Sleeping && + item.pumpDeviceState !== PumpDeviceState.Active && + item.pumpDeviceState !== PumpDeviceState.WakingUp + + companion object { + + private const val EMPTY = "-" + } +} diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/di/RileyLinkModule.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/di/RileyLinkModule.kt index 095a50ec0c85..46bb36dfa9ff 100644 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/di/RileyLinkModule.kt +++ b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/di/RileyLinkModule.kt @@ -1,13 +1,9 @@ package app.aaps.pump.common.di -import app.aaps.pump.common.dialog.RileyLinkBLEConfigActivity import app.aaps.pump.common.hw.rileylink.ble.RFSpy import app.aaps.pump.common.hw.rileylink.ble.RileyLinkBLE import app.aaps.pump.common.hw.rileylink.ble.data.RadioResponse import app.aaps.pump.common.hw.rileylink.ble.device.OrangeLinkImpl -import app.aaps.pump.common.hw.rileylink.dialog.RileyLinkStatusActivity -import app.aaps.pump.common.hw.rileylink.dialog.RileyLinkStatusGeneralFragment -import app.aaps.pump.common.hw.rileylink.dialog.RileyLinkStatusHistoryFragment import app.aaps.pump.common.hw.rileylink.service.RileyLinkBluetoothStateReceiver import app.aaps.pump.common.hw.rileylink.service.RileyLinkBroadcastReceiver import app.aaps.pump.common.hw.rileylink.service.RileyLinkService @@ -38,12 +34,6 @@ abstract class RileyLinkModule { @ContributesAndroidInjector abstract fun rfSpyProvider(): RFSpy @ContributesAndroidInjector abstract fun orangeLinkDeviceProvider(): OrangeLinkImpl - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusGeneral(): RileyLinkStatusGeneralFragment - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusHistoryFragment(): RileyLinkStatusHistoryFragment - - @ContributesAndroidInjector abstract fun contributesRileyLinkStatusActivity(): RileyLinkStatusActivity - @ContributesAndroidInjector abstract fun contributesRileyLinkBLEConfigActivity(): RileyLinkBLEConfigActivity - @ContributesAndroidInjector abstract fun contributesRileyLinkService(): RileyLinkService @ContributesAndroidInjector abstract fun contributesRileyLinkBroadcastReceiver(): RileyLinkBroadcastReceiver @ContributesAndroidInjector abstract fun contributesRileyLinkBluetoothStateReceiver(): RileyLinkBluetoothStateReceiver diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/dialog/RileyLinkBLEConfigActivity.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/dialog/RileyLinkBLEConfigActivity.kt deleted file mode 100644 index cbe9a21cfef6..000000000000 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/dialog/RileyLinkBLEConfigActivity.kt +++ /dev/null @@ -1,320 +0,0 @@ -package app.aaps.pump.common.dialog - -import android.Manifest -import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothManager -import android.bluetooth.le.BluetoothLeScanner -import android.bluetooth.le.ScanCallback -import android.bluetooth.le.ScanFilter -import android.bluetooth.le.ScanResult -import android.bluetooth.le.ScanSettings -import android.content.Context -import android.content.pm.PackageManager -import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread -import android.os.ParcelUuid -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.AdapterView.OnItemClickListener -import android.widget.BaseAdapter -import android.widget.TextView -import android.widget.Toast -import androidx.core.app.ActivityCompat -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.pump.BlePreCheck -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.ui.UiInteraction -import app.aaps.core.keys.interfaces.Preferences -import app.aaps.core.ui.activities.TranslatedDaggerAppCompatActivity -import app.aaps.pump.common.hw.rileylink.R -import app.aaps.pump.common.hw.rileylink.RileyLinkConst -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.databinding.RileyLinkBleConfigActivityBinding -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 org.apache.commons.lang3.StringUtils -import java.util.Locale -import javax.inject.Inject - -// IMPORTANT: This activity needs to be called from RileyLinkSelectPreference (see pref_medtronic.xml as example) -class RileyLinkBLEConfigActivity : TranslatedDaggerAppCompatActivity() { - - @Inject lateinit var preferences: Preferences - @Inject lateinit var blePreCheck: BlePreCheck - @Inject lateinit var rileyLinkUtil: RileyLinkUtil - @Inject lateinit var activePlugin: ActivePlugin - @Inject lateinit var context: Context - @Inject lateinit var rh: ResourceHelper - @Inject lateinit var aapsLogger: AAPSLogger - @Inject lateinit var uiInteraction: UiInteraction - - private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) - private val bluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter - private var deviceListAdapter = LeDeviceListAdapter() - private var settings: ScanSettings? = null - private var filters: List? = null - private var bleScanner: BluetoothLeScanner? = null - private var scanning = false - - private lateinit var binding: RileyLinkBleConfigActivityBinding - private val stopScanAfterTimeoutRunnable = Runnable { - if (scanning) { - stopLeDeviceScan() - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkNewAddressSet) // Reconnect current RL - } - } - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = RileyLinkBleConfigActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - - title = rh.gs(R.string.rileylink_configuration) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowHomeEnabled(true) - - // Initializes Bluetooth adapter. - binding.rileyLinkBleConfigScanDeviceList.adapter = deviceListAdapter - binding.rileyLinkBleConfigScanDeviceList.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, view: View, _: Int, _: Long -> - // stop scanning if still active - if (scanning) stopLeDeviceScan() - - val bleAddress = view.findViewById(R.id.riley_link_ble_config_scan_item_device_address)?.text.toString() - val deviceName = view.findViewById(R.id.riley_link_ble_config_scan_item_device_name)?.text.toString() - preferences.put(RileyLinkStringPreferenceKey.MacAddress, bleAddress) - preferences.put(RileyLinkStringKey.Name, deviceName) - val rileyLinkPump = activePlugin.activePumpInternal as RileyLinkPumpDevice - rileyLinkPump.rileyLinkService?.verifyConfiguration(true) // force reloading of address to assure that the RL gets reconnected (even if the address didn't change) - rileyLinkPump.triggerPumpConfigurationChangedEvent() - finish() - } - binding.rileyLinkBleConfigScanStart.setOnClickListener { - // disable currently selected RL, so that we can discover it - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkDisconnect) - startLeDeviceScan() - } - binding.rileyLinkBleConfigButtonScanStop.setOnClickListener { - if (scanning) { - stopLeDeviceScan() - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkNewAddressSet) // Reconnect current RL - } - } - binding.rileyLinkBleConfigButtonRemoveRileyLink.setOnClickListener { - uiInteraction.showOkCancelDialog( - context = this@RileyLinkBLEConfigActivity, - title = R.string.riley_link_ble_config_remove_riley_link_confirmation_title, - message = R.string.riley_link_ble_config_remove_riley_link_confirmation, - ok = { - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkDisconnect) - preferences.remove(RileyLinkStringPreferenceKey.MacAddress) - preferences.remove(RileyLinkStringKey.Name) - updateCurrentlySelectedRileyLink() - }) - } - } - - private fun updateCurrentlySelectedRileyLink() { - val address = preferences.get(RileyLinkStringPreferenceKey.MacAddress) - if (StringUtils.isEmpty(address)) { - binding.rileyLinkBleConfigCurrentlySelectedRileyLinkName.setText(R.string.riley_link_ble_config_no_riley_link_selected) - binding.rileyLinkBleConfigCurrentlySelectedRileyLinkAddress.visibility = View.GONE - binding.rileyLinkBleConfigButtonRemoveRileyLink.visibility = View.GONE - } else { - binding.rileyLinkBleConfigCurrentlySelectedRileyLinkAddress.visibility = View.VISIBLE - binding.rileyLinkBleConfigButtonRemoveRileyLink.visibility = View.VISIBLE - binding.rileyLinkBleConfigCurrentlySelectedRileyLinkName.text = preferences.get(RileyLinkStringKey.Name) - binding.rileyLinkBleConfigCurrentlySelectedRileyLinkAddress.text = address - } - } - - override fun onResume() { - super.onResume() - prepareForScanning() - updateCurrentlySelectedRileyLink() - } - - override fun onPause() { - super.onPause() - handler.removeCallbacksAndMessages(null) - } - - override fun onDestroy() { - super.onDestroy() - binding.rileyLinkBleConfigScanDeviceList.adapter = null - binding.rileyLinkBleConfigScanDeviceList.onItemClickListener = null - binding.rileyLinkBleConfigScanStart.setOnClickListener(null) - binding.rileyLinkBleConfigButtonScanStop.setOnClickListener(null) - binding.rileyLinkBleConfigButtonRemoveRileyLink.setOnClickListener(null) - if (scanning) { - stopLeDeviceScan() - rileyLinkUtil.sendBroadcastMessage(RileyLinkConst.Intents.RileyLinkNewAddressSet) // Reconnect current RL - } - handler.removeCallbacksAndMessages(null) - handler.looper.quitSafely() - } - - private fun prepareForScanning() { - val checkOK = blePreCheck.prerequisitesCheck(this) - if (checkOK) { - bleScanner = bluetoothAdapter?.bluetoothLeScanner - settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() - filters = listOf( - ScanFilter.Builder().setServiceUuid( - ParcelUuid.fromString(GattAttributes.SERVICE_RADIO) - ).build() - ) - } - } - - private val bleScanCallback: ScanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, scanRecord: ScanResult) { - aapsLogger.debug(LTag.PUMPBTCOMM, scanRecord.toString()) - runOnUiThread { if (addDevice(scanRecord)) deviceListAdapter.notifyDataSetChanged() } - } - - override fun onBatchScanResults(results: List) { - runOnUiThread { - var added = false - for (result in results) { - if (addDevice(result)) added = true - } - if (added) deviceListAdapter.notifyDataSetChanged() - } - } - - private fun addDevice(result: ScanResult): Boolean { - val device = result.device - val serviceUuids = result.scanRecord?.serviceUuids - if (serviceUuids == null || serviceUuids.isEmpty()) { - aapsLogger.debug(LTag.PUMPBTCOMM, "Device " + device.address + " has no serviceUuids (Not RileyLink).") - } else if (serviceUuids.size > 1) { - aapsLogger.debug(LTag.PUMPBTCOMM, "Device " + device.address + " has too many serviceUuids (Not RileyLink).") - } else { - val uuid = serviceUuids[0].uuid.toString().lowercase(Locale.getDefault()) - if (uuid == GattAttributes.SERVICE_RADIO) { - aapsLogger.debug(LTag.PUMPBTCOMM, "Found RileyLink with address: " + device.address) - deviceListAdapter.addDevice(result) - return true - } else { - aapsLogger.debug(LTag.PUMPBTCOMM, "Device " + device.address + " has incorrect uuid (Not RileyLink).") - } - } - return false - } - - override fun onScanFailed(errorCode: Int) { - aapsLogger.error(LTag.PUMPBTCOMM, "Scan Failed", "Error Code: $errorCode") - Toast.makeText( - this@RileyLinkBLEConfigActivity, rh.gs(R.string.riley_link_ble_config_scan_error, errorCode), - Toast.LENGTH_LONG - ).show() - } - } - - private fun startLeDeviceScan() { - if (bleScanner == null) { - aapsLogger.error(LTag.PUMPBTCOMM, "startLeDeviceScan failed: bleScanner is null") - return - } - deviceListAdapter.clear() - deviceListAdapter.notifyDataSetChanged() - handler.postDelayed(stopScanAfterTimeoutRunnable, SCAN_PERIOD_MILLIS) - runOnUiThread { - binding.rileyLinkBleConfigScanStart.isEnabled = false - binding.rileyLinkBleConfigButtonScanStop.visibility = View.VISIBLE - } - scanning = true - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { - if (bluetoothAdapter?.isEnabled == true && bluetoothAdapter?.state == BluetoothAdapter.STATE_ON) { - bleScanner?.startScan(filters, settings, bleScanCallback) - aapsLogger.debug(LTag.PUMPBTCOMM, "startLeDeviceScan: Scanning Start") - Toast.makeText(this@RileyLinkBLEConfigActivity, R.string.riley_link_ble_config_scan_scanning, Toast.LENGTH_SHORT).show() - } - } - } - - private fun stopLeDeviceScan() { - if (scanning) { - scanning = false - if (bluetoothAdapter?.isEnabled == true && bluetoothAdapter?.state == BluetoothAdapter.STATE_ON) - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { - bleScanner?.stopScan(bleScanCallback) - } - aapsLogger.debug(LTag.PUMPBTCOMM, "stopLeDeviceScan: Scanning Stop") - Toast.makeText(this, R.string.riley_link_ble_config_scan_finished, Toast.LENGTH_SHORT).show() - handler.removeCallbacks(stopScanAfterTimeoutRunnable) - } - runOnUiThread { - binding.rileyLinkBleConfigScanStart.isEnabled = true - binding.rileyLinkBleConfigButtonScanStop.visibility = View.GONE - } - } - - private inner class LeDeviceListAdapter : BaseAdapter() { - - private val leDevices: ArrayList = ArrayList() - private val rileyLinkDevices: MutableMap = HashMap() - - fun addDevice(result: ScanResult) { - if (!leDevices.contains(result.device)) { - leDevices.add(result.device) - } - rileyLinkDevices[result.device] = result.rssi - notifyDataSetChanged() - } - - fun clear() { - leDevices.clear() - rileyLinkDevices.clear() - notifyDataSetChanged() - } - - override fun getCount(): Int = leDevices.size - override fun getItem(i: Int): Any = leDevices[i] - override fun getItemId(i: Int): Long = i.toLong() - - @SuppressLint("InflateParams", "MissingPermission") - override fun getView(i: Int, v: View?, viewGroup: ViewGroup): View { - var view = v - val viewHolder: ViewHolder - // General ListView optimization code. - if (view == null) { - view = View.inflate(applicationContext, R.layout.riley_link_ble_config_scan_item, null) - viewHolder = ViewHolder(view) - view.tag = viewHolder - } else viewHolder = view.tag as ViewHolder - - val device = leDevices[i] - var deviceName = device.name - if (StringUtils.isBlank(deviceName)) deviceName = "RileyLink (?)" - deviceName += " [" + rileyLinkDevices[device] + "]" - val currentlySelectedAddress = preferences.get(RileyLinkStringPreferenceKey.MacAddress) - if (currentlySelectedAddress == device.address) { - deviceName += " (" + resources.getString(R.string.riley_link_ble_config_scan_selected) + ")" - } - viewHolder.deviceName.text = deviceName - viewHolder.deviceAddress.text = device.address - return view as View - } - } - - internal class ViewHolder(view: View) { - - val deviceName: TextView = view.findViewById(R.id.riley_link_ble_config_scan_item_device_name) - val deviceAddress: TextView = view.findViewById(R.id.riley_link_ble_config_scan_item_device_address) - } - - companion object { - - private const val SCAN_PERIOD_MILLIS: Long = 15000 - } - -} diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusActivity.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusActivity.kt deleted file mode 100644 index 0d0cb4019e38..000000000000 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.aaps.pump.common.hw.rileylink.dialog - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.ui.activities.TranslatedDaggerAppCompatActivity -import app.aaps.pump.common.hw.rileylink.R -import app.aaps.pump.common.hw.rileylink.databinding.RileylinkStatusBinding -import com.google.android.material.tabs.TabLayoutMediator -import javax.inject.Inject - -class RileyLinkStatusActivity : TranslatedDaggerAppCompatActivity() { - - @Inject lateinit var rh: ResourceHelper - - private lateinit var binding: RileylinkStatusBinding - - private var sectionsPagerAdapter: SectionsPagerAdapter? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = RileylinkStatusBinding.inflate(layoutInflater) - setContentView(binding.root) - - sectionsPagerAdapter = SectionsPagerAdapter(this) - sectionsPagerAdapter?.addFragment(RileyLinkStatusGeneralFragment::class.java.name, rh.gs(R.string.rileylink_settings_tab1)) - sectionsPagerAdapter?.addFragment(RileyLinkStatusHistoryFragment::class.java.name, rh.gs(R.string.rileylink_settings_tab2)) - - binding.pager.adapter = sectionsPagerAdapter - - TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> - tab.text = sectionsPagerAdapter?.getPageTitle(position) - }.attach() - } - - override fun onDestroy() { - super.onDestroy() - binding.pager.adapter = null - binding.tabLayout.clearOnTabSelectedListeners() - } - - class SectionsPagerAdapter(private val activity: AppCompatActivity) : FragmentStateAdapter(activity) { - - private val fragmentList: MutableList = ArrayList() - private val fragmentTitle: MutableList = ArrayList() - override fun getItemCount(): Int = fragmentList.size - override fun createFragment(position: Int): Fragment = - activity.supportFragmentManager.fragmentFactory.instantiate(ClassLoader.getSystemClassLoader(), fragmentList[position]) - - fun getPageTitle(position: Int): CharSequence = fragmentTitle[position] - fun addFragment(fragment: String, title: String) { - fragmentList.add(fragment) - fragmentTitle.add(title) - } - } -} \ No newline at end of file diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.kt deleted file mode 100644 index 64c17f7f6a09..000000000000 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusGeneralFragment.kt +++ /dev/null @@ -1,97 +0,0 @@ -package app.aaps.pump.common.hw.rileylink.dialog - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import app.aaps.core.interfaces.plugin.ActivePlugin -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.core.keys.interfaces.Preferences -import app.aaps.pump.common.hw.rileylink.R -import app.aaps.pump.common.hw.rileylink.databinding.RileylinkStatusGeneralBinding -import app.aaps.pump.common.hw.rileylink.defs.RileyLinkPumpDevice -import app.aaps.pump.common.hw.rileylink.defs.RileyLinkTargetDevice -import app.aaps.pump.common.hw.rileylink.keys.RileylinkBooleanPreferenceKey -import app.aaps.pump.common.hw.rileylink.service.RileyLinkServiceData -import dagger.android.support.DaggerFragment -import javax.inject.Inject - -class RileyLinkStatusGeneralFragment : DaggerFragment() { - - @Inject lateinit var activePlugin: ActivePlugin - @Inject lateinit var rh: ResourceHelper - @Inject lateinit var rileyLinkServiceData: RileyLinkServiceData - @Inject lateinit var dateUtil: DateUtil - @Inject lateinit var preferences: Preferences - - private var _binding: RileylinkStatusGeneralBinding? = null - - // This property is only valid between onCreateView and onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - RileylinkStatusGeneralBinding.inflate(inflater, container, false).also { _binding = it }.root - - override fun onResume() { - super.onResume() - refreshData() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.refresh.setOnClickListener { refreshData() } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun refreshData() { - val targetDevice = rileyLinkServiceData.targetDevice - binding.connectionStatus.text = rh.gs(rileyLinkServiceData.rileyLinkServiceState.resourceId) - binding.configuredRileyLinkAddress.text = rileyLinkServiceData.rileyLinkAddress ?: EMPTY - binding.configuredRileyLinkName.text = rileyLinkServiceData.rileyLinkName ?: EMPTY - if (preferences.get(RileylinkBooleanPreferenceKey.ShowReportedBatteryLevel)) { - binding.batteryLevelRow.visibility = View.VISIBLE - val batteryLevel = rileyLinkServiceData.batteryLevel - binding.batteryLevel.text = batteryLevel?.let { rh.gs(R.string.rileylink_battery_level_value, it) } ?: EMPTY - } else binding.batteryLevelRow.visibility = View.GONE - binding.connectionError.text = rileyLinkServiceData.rileyLinkError?.let { rh.gs(it.getResourceId(targetDevice)) } ?: EMPTY - if (rileyLinkServiceData.isOrange && rileyLinkServiceData.versionOrangeFirmware != null) { - binding.firmwareVersion.text = rh.gs( - R.string.rileylink_firmware_version_value_orange, - rileyLinkServiceData.versionOrangeFirmware, - rileyLinkServiceData.versionOrangeHardware ?: EMPTY - ) - } else { - binding.firmwareVersion.text = rh.gs( - R.string.rileylink_firmware_version_value, - rileyLinkServiceData.versionBLE113 ?: EMPTY, - rileyLinkServiceData.versionCC110 ?: EMPTY - ) - } - val rileyLinkPumpDevice = activePlugin.activePumpInternal as RileyLinkPumpDevice - val rileyLinkPumpInfo = rileyLinkPumpDevice.pumpInfo - binding.deviceType.setText(targetDevice.resourceId) - if (targetDevice == RileyLinkTargetDevice.MedtronicPump) { - binding.connectedDeviceDetails.visibility = View.VISIBLE - binding.configuredDeviceModel.text = activePlugin.activePumpInternal.pumpDescription.pumpType.description - binding.connectedDeviceModel.text = rileyLinkPumpInfo.connectedDeviceModel - } else binding.connectedDeviceDetails.visibility = View.GONE - binding.serialNumber.text = rileyLinkPumpInfo.connectedDeviceSerialNumber - binding.pumpFrequency.text = rileyLinkPumpInfo.pumpFrequency - if (rileyLinkServiceData.lastGoodFrequency != null) { - binding.lastUsedFrequency.text = rh.gs(R.string.rileylink_pump_frequency_value, rileyLinkServiceData.lastGoodFrequency) - } - val lastConnectionTimeMillis = rileyLinkPumpDevice.lastConnectionTimeMillis - if (lastConnectionTimeMillis == 0L) binding.lastDeviceContact.text = rh.gs(R.string.riley_link_ble_config_connected_never) - else binding.lastDeviceContact.text = dateUtil.dateAndTimeAndSecondsString(lastConnectionTimeMillis) - } - - companion object { - - private const val EMPTY = "-" - } -} \ No newline at end of file diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusHistoryFragment.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusHistoryFragment.kt deleted file mode 100644 index 976e01709bb6..000000000000 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/dialog/RileyLinkStatusHistoryFragment.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.aaps.pump.common.hw.rileylink.dialog - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import app.aaps.core.interfaces.pump.defs.PumpDeviceState -import app.aaps.core.interfaces.resources.ResourceHelper -import app.aaps.core.interfaces.utils.DateUtil -import app.aaps.pump.common.hw.rileylink.R -import app.aaps.pump.common.hw.rileylink.RileyLinkUtil -import app.aaps.pump.common.hw.rileylink.data.RLHistoryItem -import app.aaps.pump.common.hw.rileylink.databinding.RileylinkStatusHistoryBinding -import app.aaps.pump.common.hw.rileylink.databinding.RileylinkStatusHistoryItemBinding -import dagger.android.support.DaggerFragment -import javax.inject.Inject - -class RileyLinkStatusHistoryFragment : DaggerFragment() { - - @Inject lateinit var rileyLinkUtil: RileyLinkUtil - @Inject lateinit var rh: ResourceHelper - @Inject lateinit var dateUtil: DateUtil - - private var _binding: RileylinkStatusHistoryBinding? = null - - // This property is only valid between onCreateView and onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - RileylinkStatusHistoryBinding.inflate(inflater, container, false).also { _binding = it }.root - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.historyList.setHasFixedSize(true) - binding.historyList.layoutManager = LinearLayoutManager(view.context) - binding.refresh.setOnClickListener { refreshData() } - } - - override fun onResume() { - super.onResume() - refreshData() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun refreshData() { - binding.historyList.adapter = - RecyclerViewAdapter(rileyLinkUtil.rileyLinkHistory.filter { isValidItem(it) }.sortedWith(RLHistoryItem.Comparator())) - } - - private fun isValidItem(item: RLHistoryItem): Boolean = - item.pumpDeviceState !== PumpDeviceState.Sleeping && - item.pumpDeviceState !== PumpDeviceState.Active && - item.pumpDeviceState !== PumpDeviceState.WakingUp - - inner class RecyclerViewAdapter internal constructor(private val historyList: List) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): HistoryViewHolder { - val v = LayoutInflater.from(viewGroup.context).inflate(R.layout.rileylink_status_history_item, viewGroup, false) - return HistoryViewHolder(v) - } - - override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { - val item = historyList[position] - holder.binding.historyTime.text = dateUtil.dateAndTimeAndSecondsString(item.dateTime.toDateTime().millis) - holder.binding.historySource.text = item.source.desc - holder.binding.historyDescription.text = item.getDescription(rh) - } - - override fun getItemCount(): Int = historyList.size - - inner class HistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - val binding = RileylinkStatusHistoryItemBinding.bind(itemView) - } - } -} \ No newline at end of file diff --git a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkIntentPreferenceKey.kt b/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkIntentPreferenceKey.kt deleted file mode 100644 index 3d866b6a9771..000000000000 --- a/pump/rileylink/src/main/kotlin/app/aaps/pump/common/hw/rileylink/keys/RileyLinkIntentPreferenceKey.kt +++ /dev/null @@ -1,30 +0,0 @@ -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.IntentPreferenceKey -import app.aaps.pump.common.dialog.RileyLinkBLEConfigActivity -import app.aaps.pump.common.hw.rileylink.R - -enum class RileyLinkIntentPreferenceKey( - override val key: String, - override val titleResId: Int = 0, - override val summaryResId: Int? = null, - override val preferenceType: PreferenceType = PreferenceType.ACTIVITY, - override val activityClass: Class<*>? = null, - override val defaultedBySM: Boolean = false, - override val showInApsMode: Boolean = true, - override val showInNsClientMode: Boolean = true, - override val showInPumpControlMode: Boolean = true, - override val dependency: BooleanPreferenceKey? = null, - override val negativeDependency: BooleanPreferenceKey? = null, - override val hideParentScreenIfHidden: Boolean = false, - override val exportable: Boolean = false -) : IntentPreferenceKey { - - MacAddressSelector( - key = "rileylink_mac_address_selector", - titleResId = R.string.rileylink_configuration, - activityClass = RileyLinkBLEConfigActivity::class.java - ) -} \ No newline at end of file diff --git a/pump/rileylink/src/main/res/layout/riley_link_ble_config_activity.xml b/pump/rileylink/src/main/res/layout/riley_link_ble_config_activity.xml deleted file mode 100644 index c4601610fbc2..000000000000 --- a/pump/rileylink/src/main/res/layout/riley_link_ble_config_activity.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/riley_link_ble_config_scan_item.xml b/pump/rileylink/src/main/res/layout/riley_link_ble_config_scan_item.xml deleted file mode 100644 index b4fb7985e5c0..000000000000 --- a/pump/rileylink/src/main/res/layout/riley_link_ble_config_scan_item.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/rileylink_status.xml b/pump/rileylink/src/main/res/layout/rileylink_status.xml deleted file mode 100644 index 4bcf02cf16fd..000000000000 --- a/pump/rileylink/src/main/res/layout/rileylink_status.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/rileylink_status_device_item.xml b/pump/rileylink/src/main/res/layout/rileylink_status_device_item.xml deleted file mode 100644 index f37596f31f91..000000000000 --- a/pump/rileylink/src/main/res/layout/rileylink_status_device_item.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/rileylink_status_general.xml b/pump/rileylink/src/main/res/layout/rileylink_status_general.xml deleted file mode 100644 index 53a87e800f27..000000000000 --- a/pump/rileylink/src/main/res/layout/rileylink_status_general.xml +++ /dev/null @@ -1,472 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/rileylink_status_history.xml b/pump/rileylink/src/main/res/layout/rileylink_status_history.xml deleted file mode 100644 index 9de9eb28f040..000000000000 --- a/pump/rileylink/src/main/res/layout/rileylink_status_history.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - diff --git a/pump/rileylink/src/main/res/layout/rileylink_status_history_item.xml b/pump/rileylink/src/main/res/layout/rileylink_status_history_item.xml deleted file mode 100644 index f7098d42a8b5..000000000000 --- a/pump/rileylink/src/main/res/layout/rileylink_status_history_item.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - diff --git a/pump/rileylink/src/main/res/values/strings.xml b/pump/rileylink/src/main/res/values/strings.xml index fde898ce8dbb..35875fbbf1b6 100644 --- a/pump/rileylink/src/main/res/values/strings.xml +++ b/pump/rileylink/src/main/res/values/strings.xml @@ -18,26 +18,14 @@ Worldwide (868 Mhz) - Scan - Stop - Selected - RileyLink Scan Scanning - Scanning finished - Scanning error: %1$d Never - Currently Selected RileyLink - Remove - Are you sure that you want to remove your RileyLink? - Remove RileyLink - No RileyLink selected Settings History RileyLink Status Pump Status - RileyLink Settings RileyLink Address: Name: