diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt index e91a7893..517885a4 100644 --- a/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt @@ -1,15 +1,19 @@ package eu.darken.capod.common.settings +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,33 +21,61 @@ import androidx.compose.ui.unit.dp import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper +enum class InfoBoxType { INFO, WARNING } + @Composable fun SettingsInfoBox( text: String, modifier: Modifier = Modifier, + type: InfoBoxType = InfoBoxType.INFO, + action: @Composable (() -> Unit)? = null, ) { + val containerColor = when (type) { + InfoBoxType.INFO -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + InfoBoxType.WARNING -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f) + } + val iconTint = when (type) { + InfoBoxType.INFO -> MaterialTheme.colorScheme.primary + InfoBoxType.WARNING -> MaterialTheme.colorScheme.tertiary + } + val icon = when (type) { + InfoBoxType.INFO -> Icons.Outlined.Info + InfoBoxType.WARNING -> Icons.Outlined.Warning + } + Surface( - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f), + color = containerColor, shape = RoundedCornerShape(12.dp), modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) { Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.Top, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = if (action != null) 8.dp else 16.dp), + verticalAlignment = if (action != null) Alignment.Top else Alignment.CenterVertically, ) { Icon( - imageVector = Icons.Outlined.Info, + imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.primary, + tint = iconTint, modifier = Modifier - .padding(end = 10.dp, top = 2.dp) + .padding(end = 10.dp) .size(20.dp), ) - Text( - text = text, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), - ) + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = if (action != null) Alignment.End else Alignment.Start, + ) { + Text( + text = text, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), + ) + if (action != null) { + action() + } + } } } } @@ -55,3 +87,26 @@ private fun SettingsInfoBoxPreview() = PreviewWrapper { text = "If ear detection only works for one pod, this is an Apple limitation. Only the \"primary pod\" (used for microphone) is detected.", ) } + +@Preview2 +@Composable +private fun SettingsInfoBoxWarningPreview() = PreviewWrapper { + SettingsInfoBox( + text = "This feature requires the monitor to be running.", + type = InfoBoxType.WARNING, + ) +} + +@Preview2 +@Composable +private fun SettingsInfoBoxWarningWithActionPreview() = PreviewWrapper { + SettingsInfoBox( + text = "Popups require the monitor to be running. Your monitor mode is set to \"When app is open\", which stops the monitor when you leave the app.", + type = InfoBoxType.WARNING, + action = { + TextButton(onClick = {}) { + Text("Fix it") + } + }, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt index 741c54f3..8c53917d 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt @@ -32,6 +32,7 @@ import eu.darken.capod.common.uix.Activity2 import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.currentThemeState import eu.darken.capod.main.core.themeState +import eu.darken.capod.reaction.ui.popup.PopUpWindow import javax.inject.Inject import eu.darken.capod.common.datastore.valueBlocking @@ -41,6 +42,7 @@ class MainActivity : Activity2() { @Inject lateinit var navCtrl: NavigationController @Inject lateinit var navigationEntries: Set<@JvmSuppressWildcards NavigationEntry> @Inject lateinit var generalSettings: GeneralSettings + @Inject lateinit var popUpWindow: PopUpWindow override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,6 +100,17 @@ class MainActivity : Activity2() { } } + override fun onResume() { + super.onResume() + popUpWindow.isMainActivityVisible = true + popUpWindow.close() + } + + override fun onPause() { + popUpWindow.isMainActivityVisible = false + super.onPause() + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) consumeUpgradeExtra(intent) diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index a3d1682b..35557ed6 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -86,6 +86,8 @@ import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsCategoryHeader +import eu.darken.capod.main.core.MonitorMode +import eu.darken.capod.common.settings.InfoBoxType import eu.darken.capod.common.settings.SettingsInfoBox import eu.darken.capod.common.settings.SettingsPreferenceItem import eu.darken.capod.common.settings.SettingsSection @@ -168,6 +170,7 @@ fun DeviceSettingsScreenHost( onAutoConnectConditionChange = { vm.setAutoConnectCondition(it) }, onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) }, onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) }, + onFixMonitorMode = { vm.setMonitorModeAutomatic() }, ) } @@ -204,6 +207,7 @@ fun DeviceSettingsScreen( onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {}, onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {}, onShowPopUpOnConnectionChange: (Boolean) -> Unit = {}, + onFixMonitorMode: () -> Unit = {}, ) { val device = state.device val features = device?.model?.features @@ -377,6 +381,22 @@ fun DeviceSettingsScreen( onCheckedChange = onShowPopUpOnConnectionChange, requiresUpgrade = !isPro, ) + val anyPopupEnabled = reactions.showPopUpOnCaseOpen || reactions.showPopUpOnConnection + if (anyPopupEnabled && state.monitorMode == MonitorMode.MANUAL) { + SettingsInfoBox( + text = stringResource(R.string.settings_popup_warning_manual_mode), + type = InfoBoxType.WARNING, + action = { + TextButton(onClick = onFixMonitorMode) { + Text(stringResource(R.string.general_fix_it_action)) + } + }, + ) + } else if (anyPopupEnabled) { + SettingsInfoBox( + text = stringResource(R.string.settings_popup_info_not_in_app), + ) + } } } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt index 1adc2282..c5f9f47b 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt @@ -87,7 +87,14 @@ class DeviceSettingsViewModel @Inject constructor( // The Bluetooth HEADSET profile lookup can be slow to produce its first value. // Seed this branch so the screen can render immediately after navigation. bluetoothManager.connectedDevices.onStart { emit(emptyList()) }, - ) { _, device, upgrade, forcing, connectedDevices -> + generalSettings.monitorMode.flow, + ) { args -> + val device = args[1] as PodDevice? + val upgrade = args[2] as eu.darken.capod.common.upgrade.UpgradeRepo.Info + val forcing = args[3] as Boolean + @Suppress("UNCHECKED_CAST") + val connectedDevices = args[4] as Collection + val monitorMode = args[5] as MonitorMode val connectedAddresses = connectedDevices.map { it.address }.toSet() State( device = device, @@ -96,6 +103,7 @@ class DeviceSettingsViewModel @Inject constructor( isNudgeAvailable = bluetoothManager.isNudgeAvailable, isForceConnecting = forcing, isClassicallyConnected = device?.address?.let { it in connectedAddresses } == true, + monitorMode = monitorMode, ) } }.asLiveState() @@ -118,6 +126,7 @@ class DeviceSettingsViewModel @Inject constructor( val isNudgeAvailable: Boolean = true, val isForceConnecting: Boolean = false, val isClassicallyConnected: Boolean = false, + val monitorMode: MonitorMode = MonitorMode.AUTOMATIC, ) { val reactions: ReactionConfig get() = device?.reactions ?: ReactionConfig() } @@ -316,6 +325,10 @@ class DeviceSettingsViewModel @Inject constructor( fun setShowPopUpOnConnection(enabled: Boolean) = proGatedReaction(enabled) { it.copy(showPopUpOnConnection = enabled) } + fun setMonitorModeAutomatic() = launch { + generalSettings.monitorMode.value(MonitorMode.AUTOMATIC) + } + fun navToStemConfig() = launch { if (upgradeRepo.isPro()) { navTo(Nav.Main.StemActionConfig) diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt index 80ed16db..72faf30a 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt @@ -52,7 +52,13 @@ class PopUpWindow @Inject constructor( private var lifecycleOwner: OverlayLifecycleOwner? = null private var deviceState: MutableState? = null + @Volatile var isMainActivityVisible: Boolean = false + fun show(device: PodDevice) { + if (isMainActivityVisible) { + log(TAG) { "Suppressing popup, MainActivity is visible" } + return + } try { log(TAG) { "open()" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f5bc27dc..e4101b29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,6 +85,9 @@ Show a popup when the device case is opened (experimental). Show connection popup Show a popup when the device connects for the first time. + Popups are only shown when you are not in the app. + Popups require the monitor to be running. Your monitor mode is set to \"When app is open\", which stops the monitor when you leave the app. + Fix it Device status Connected device