From 1da4938825e2919330272e1b7a275d15c915c813 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 09:20:23 +0000 Subject: [PATCH 1/3] #2184 feat: add display resolution constraint Add a new constraint that is satisfied when the device's real display resolution matches a chosen width and height. The constraint is configured in a bottom sheet that shows a row of selectable chips for each of the display's supported resolution modes, plus a "Custom" chip that reveals side-by-side width and height text fields. When the display reports one or zero supported modes, the text fields are shown immediately. The real display size is read from DisplayAdapter.size and compared ignoring orientation so the constraint holds in both portrait and landscape. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011GqGRM7xSwYjxKYT6gp66F --- CHANGELOG.md | 2 + .../constraints/ChooseConstraintScreen.kt | 1 + .../constraints/ChooseConstraintViewModel.kt | 18 ++ .../keymapper/base/constraints/Constraint.kt | 29 ++ .../base/constraints/ConstraintId.kt | 2 + .../base/constraints/ConstraintSnapshot.kt | 15 + .../base/constraints/ConstraintUiHelper.kt | 5 + .../base/constraints/ConstraintUtils.kt | 5 + .../constraints/CreateConstraintUseCase.kt | 10 + .../DisplayResolutionConstraintBottomSheet.kt | 292 ++++++++++++++++++ .../KeyMapConstraintsComparator.kt | 3 + base/src/main/res/values/strings.xml | 6 + .../base/utils/TestConstraintSnapshot.kt | 5 + .../data/entities/ConstraintEntity.kt | 5 + .../system/display/AndroidDisplayAdapter.kt | 8 + .../system/display/DisplayAdapter.kt | 6 + 16 files changed, 412 insertions(+) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 57be1aaf55..b73a3d80cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Added +- #2184 Add a display resolution constraint. Pick from the display's supported resolutions or enter a + custom width and height. - #2163 Add ringer mode constraints (Ring, Vibrate, Silent). - #2140 Add monochrome app icon layer for themed icon support on Android 13+. - #2174 Add "Do not remap by default" preference to the default options settings page. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt index 857f30330c..fb243994b2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintScreen.kt @@ -55,6 +55,7 @@ fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConst val query by viewModel.searchQuery.collectAsStateWithLifecycle() TimeConstraintBottomSheet(viewModel) + DisplayResolutionConstraintBottomSheet(viewModel) ChooseConstraintScreen( modifier = modifier, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index eb08042bab..b5e3729ef8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -95,6 +95,8 @@ class ChooseConstraintViewModel @Inject constructor( var timeConstraintState: ConstraintData.Time? by mutableStateOf(null) + var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null) + init { viewModelScope.launch { returnResult.collect { constraintData -> @@ -112,6 +114,13 @@ class ChooseConstraintViewModel @Inject constructor( } } + fun onDoneConfigDisplayResolutionClick(width: Int, height: Int) { + viewModelScope.launch { + returnResult.emit(ConstraintData.DisplayResolution(width = width, height = height)) + displayResolutionState = null + } + } + fun onNavigateBack() { viewModelScope.launch { popBackStack() @@ -152,6 +161,15 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) + ConstraintId.DISPLAY_RESOLUTION -> { + val currentResolution = useCase.getCurrentResolution() + displayResolutionState = DisplayResolutionSheetState( + supportedResolutions = useCase.getSupportedResolutions(), + initialWidth = currentResolution.width, + initialHeight = currentResolution.height, + ) + } + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> returnResult.emit(ConstraintData.OrientationPortrait) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index 7b7fb41917..f5d3c0ec57 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -105,6 +105,11 @@ sealed class ConstraintData { } } + @Serializable + data class DisplayResolution(val width: Int, val height: Int) : ConstraintData() { + override val id: ConstraintId = ConstraintId.DISPLAY_RESOLUTION + } + @Serializable data class FlashlightOn(val lens: CameraLens) : ConstraintData() { override val id: ConstraintId = ConstraintId.FLASHLIGHT_ON @@ -302,6 +307,12 @@ object ConstraintEntityMapper { return extraValue } + fun getResolutionWidth(): Int = + entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_WIDTH).valueOrNull()!!.toInt() + + fun getResolutionHeight(): Int = + entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_HEIGHT).valueOrNull()!!.toInt() + val constraintData = when (entity.type) { ConstraintEntity.APP_FOREGROUND -> ConstraintData.AppInForeground( getPackageName(), @@ -362,6 +373,11 @@ object ConstraintEntityMapper { ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED) + ConstraintEntity.DISPLAY_RESOLUTION -> ConstraintData.DisplayResolution( + width = getResolutionWidth(), + height = getResolutionHeight(), + ) + ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn @@ -575,6 +591,19 @@ object ConstraintEntityMapper { ) } + is ConstraintData.DisplayResolution -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.DISPLAY_RESOLUTION, + EntityExtra( + ConstraintEntity.EXTRA_RESOLUTION_WIDTH, + constraint.data.width.toString(), + ), + EntityExtra( + ConstraintEntity.EXTRA_RESOLUTION_HEIGHT, + constraint.data.height.toString(), + ), + ) + is ConstraintData.ScreenOff -> ConstraintEntity( uid = constraint.uid, ConstraintEntity.SCREEN_OFF, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt index 8f0512b696..944eed883d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt @@ -31,6 +31,8 @@ enum class ConstraintId { PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + DISPLAY_RESOLUTION, + FLASHLIGHT_ON, FLASHLIGHT_OFF, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index 5326f32129..0cc28dc291 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -5,6 +5,7 @@ import android.os.Build import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraAdapter @@ -51,6 +52,7 @@ class LazyConstraintSnapshot( displayAdapter.cachedPhysicalOrientation } private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() } + private val displaySize: SizeKM by lazy { displayAdapter.size } private val appsPlayingMedia: List by lazy { mediaAdapter.getActiveMediaSessionPackages() } @@ -134,6 +136,19 @@ class LazyConstraintSnapshot( is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn + + // Compare the resolution regardless of orientation so that the constraint + // holds whether the device is in portrait or landscape. + is ConstraintData.DisplayResolution -> + ( + displaySize.width == constraint.data.width && + displaySize.height == constraint.data.height + ) || + ( + displaySize.width == constraint.data.height && + displaySize.height == constraint.data.width + ) + is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens) is ConstraintData.FlashlightOn -> cameraAdapter.isFlashlightOn(constraint.data.lens) is ConstraintData.WifiConnected -> { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 16a17570f9..4990777498 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -105,6 +105,11 @@ class ConstraintUiHelper( is ConstraintData.ScreenOn -> getString(R.string.constraint_screen_on_description) + is ConstraintData.DisplayResolution -> getString( + R.string.constraint_display_resolution_description, + arrayOf(constraint.data.width, constraint.data.height), + ) + is ConstraintData.FlashlightOff -> if (constraint.data.lens == CameraLens.FRONT) { getString(R.string.constraint_front_flashlight_off_description) } else { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index daabcee482..6fae3f13f3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.constraints import androidx.annotation.StringRes import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AspectRatio import androidx.compose.material.icons.outlined.Battery2Bar import androidx.compose.material.icons.outlined.BatteryChargingFull import androidx.compose.material.icons.outlined.BluetoothConnected @@ -77,6 +78,7 @@ object ConstraintUtils { ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE, ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + ConstraintId.DISPLAY_RESOLUTION, -> ConstraintCategory.DISPLAY ConstraintId.FLASHLIGHT_ON, @@ -167,6 +169,8 @@ object ConstraintUtils { ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff) ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + ConstraintId.DISPLAY_RESOLUTION -> ComposeIconInfo.Vector(Icons.Outlined.AspectRatio) + ConstraintId.FLASHLIGHT_OFF -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOff) ConstraintId.FLASHLIGHT_ON -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOn) @@ -225,6 +229,7 @@ object ConstraintUtils { R.string.constraint_choose_bluetooth_device_disconnected ConstraintId.SCREEN_ON -> R.string.constraint_choose_screen_on_description ConstraintId.SCREEN_OFF -> R.string.constraint_choose_screen_off_description + ConstraintId.DISPLAY_RESOLUTION -> R.string.constraint_choose_display_resolution ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt index 93b1d72b9d..aa83d41020 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt @@ -3,10 +3,12 @@ package io.github.sds100.keymapper.base.constraints import android.content.pm.PackageManager import android.os.Build import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.camera.CameraLens +import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.network.NetworkAdapter @@ -20,6 +22,7 @@ class CreateConstraintUseCaseImpl @Inject constructor( private val inputMethodAdapter: InputMethodAdapter, private val preferenceRepository: PreferenceRepository, private val cameraAdapter: CameraAdapter, + private val displayAdapter: DisplayAdapter, ) : CreateConstraintUseCase { override fun isSupported(constraint: ConstraintId): KMError? { @@ -74,6 +77,10 @@ class CreateConstraintUseCaseImpl @Inject constructor( override fun getFlashlightLenses(): Set { return CameraLens.entries.filter { cameraAdapter.getFlashInfo(it) != null }.toSet() } + + override fun getSupportedResolutions(): List = displayAdapter.getSupportedResolutions() + + override fun getCurrentResolution(): SizeKM = displayAdapter.size } interface CreateConstraintUseCase { @@ -85,4 +92,7 @@ interface CreateConstraintUseCase { fun getSavedWifiSSIDs(): Flow> fun getFlashlightLenses(): Set + + fun getSupportedResolutions(): List + fun getCurrentResolution(): SizeKM } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt new file mode 100644 index 0000000000..8d8c789372 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -0,0 +1,292 @@ +package io.github.sds100.keymapper.base.constraints + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.common.utils.SizeKM +import kotlinx.coroutines.launch + +/** + * State passed to the display resolution bottom sheet. + * + * @param supportedResolutions the resolutions the display reports as supported. + * @param initialWidth the current real display width, used to prefill the custom fields. + * @param initialHeight the current real display height, used to prefill the custom fields. + */ +data class DisplayResolutionSheetState( + val supportedResolutions: List, + val initialWidth: Int, + val initialHeight: Int, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val state = viewModel.displayResolutionState ?: return + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = state, + onDismissRequest = { viewModel.displayResolutionState = null }, + onDoneClick = { width, height -> + viewModel.onDoneConfigDisplayResolutionClick(width, height) + }, + ) +} + +/** + * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 + * are treated as the same resolution. + */ +private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { + return (width == other.width && height == other.height) || + (width == other.height && height == other.width) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DisplayResolutionConstraintBottomSheet( + sheetState: SheetState, + state: DisplayResolutionSheetState, + onDismissRequest: () -> Unit = {}, + onDoneClick: (width: Int, height: Int) -> Unit = { _, _ -> }, +) { + val scope = rememberCoroutineScope() + + val initialSize = remember(state) { SizeKM(state.initialWidth, state.initialHeight) } + val matchingResolution = remember(state) { + state.supportedResolutions.firstOrNull { it.matchesIgnoringOrientation(initialSize) } + } + + // Show the text fields immediately when there is nothing meaningful to pick from + // or when the current resolution isn't one of the supported modes. + var isCustom by remember(state) { + mutableStateOf(state.supportedResolutions.size <= 1 || matchingResolution == null) + } + var selectedResolution by remember(state) { + mutableStateOf(matchingResolution ?: state.supportedResolutions.firstOrNull()) + } + var widthText by remember(state) { mutableStateOf(state.initialWidth.toString()) } + var heightText by remember(state) { mutableStateOf(state.initialHeight.toString()) } + + val customWidth = widthText.toIntOrNull() + val customHeight = heightText.toIntOrNull() + + val isValid = if (isCustom) { + customWidth != null && customWidth > 0 && customHeight != null && customHeight > 0 + } else { + selectedResolution != null + } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + dragHandle = null, + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + textAlign = TextAlign.Center, + text = stringResource(R.string.constraint_display_resolution_bottom_sheet_title), + style = MaterialTheme.typography.headlineMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // The chips are only useful when there is more than one supported mode to pick from. + if (state.supportedResolutions.size > 1) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (resolution in state.supportedResolutions) { + FilterChip( + selected = !isCustom && selectedResolution == resolution, + onClick = { + isCustom = false + selectedResolution = resolution + }, + label = { Text("${resolution.width} × ${resolution.height}") }, + ) + } + + FilterChip( + selected = isCustom, + onClick = { isCustom = true }, + label = { + Text( + stringResource( + R.string.constraint_display_resolution_custom_chip, + ), + ) + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + + if (isCustom) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedTextField( + modifier = Modifier.weight(1f), + value = widthText, + onValueChange = { widthText = it.filter(Char::isDigit) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { + Text(stringResource(R.string.constraint_display_resolution_width)) + }, + ) + + OutlinedTextField( + modifier = Modifier.weight(1f), + value = heightText, + onValueChange = { heightText = it.filter(Char::isDigit) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { + Text(stringResource(R.string.constraint_display_resolution_height)) + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Button( + modifier = Modifier.weight(1f), + enabled = isValid, + onClick = { + val width = if (isCustom) customWidth else selectedResolution?.width + val height = if (isCustom) customHeight else selectedResolution?.height + + if (width != null && height != null) { + scope.launch { + sheetState.hide() + onDoneClick(width, height) + } + } + }, + ) { + Text(stringResource(R.string.pos_done)) + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewWithModes() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = DisplayResolutionSheetState( + supportedResolutions = listOf( + SizeKM(1080, 2400), + SizeKM(1440, 3200), + ), + initialWidth = 1080, + initialHeight = 2400, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewCustomOnly() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = DisplayResolutionSheetState( + supportedResolutions = emptyList(), + initialWidth = 1080, + initialHeight = 2400, + ), + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt index ab4cb3dc31..21cfdde574 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapConstraintsComparator.kt @@ -115,6 +115,9 @@ class KeyMapConstraintsComparator( is ConstraintData.RingerMode -> Success("") is ConstraintData.ScreenOff -> Success("") is ConstraintData.ScreenOn -> Success("") + is ConstraintData.DisplayResolution -> Success( + "${constraint.data.width}x${constraint.data.height}", + ) is ConstraintData.WifiConnected -> if (constraint.data.ssid == null) { Success("") } else { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3e6dbd8012..02f183468d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -262,6 +262,12 @@ Physical: Landscape Physical: Portrait (upside down) Physical: Landscape (inverted) + Display resolution + Resolution is %1$d × %2$d + Display resolution + Custom + Width + Height App playing media App not playing media Media is playing diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt index ca6699b5c8..1af4cf04f2 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/TestConstraintSnapshot.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.base.constraints.ConstraintData import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.foldable.HingeState @@ -36,6 +37,7 @@ class TestConstraintSnapshot( val localTime: LocalTime = LocalTime.now(), val hingeState: HingeState = HingeState.Unavailable, val isNotificationPanelShowing: Boolean = false, + val displaySize: SizeKM = SizeKM(1080, 1920), ) : ConstraintSnapshot { override fun isSatisfied(constraint: Constraint): Boolean { @@ -72,6 +74,9 @@ class TestConstraintSnapshot( is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn + is ConstraintData.DisplayResolution -> + (displaySize.width == data.width && displaySize.height == data.height) || + (displaySize.width == data.height && displaySize.height == data.width) is ConstraintData.FlashlightOff -> when (data.lens) { CameraLens.BACK -> !isBackFlashlightOn CameraLens.FRONT -> !isFrontFlashlightOn diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index 0a2c60feba..9e8a884ebe 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -65,6 +65,8 @@ data class ConstraintEntity( const val PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED = "constraint_physical_orientation_landscape_inverted" + const val DISPLAY_RESOLUTION = "constraint_display_resolution" + const val FLASHLIGHT_ON = "flashlight_on" const val FLASHLIGHT_OFF = "flashlight_off" @@ -111,6 +113,9 @@ data class ConstraintEntity( const val EXTRA_IME_ID = "extra_ime_id" const val EXTRA_IME_LABEL = "extra_ime_label" + const val EXTRA_RESOLUTION_WIDTH = "extra_resolution_width" + const val EXTRA_RESOLUTION_HEIGHT = "extra_resolution_height" + /** * The time is stored in the following format: 20:25. */ diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 560e60e42d..1c3a1d0e12 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -101,6 +101,14 @@ class AndroidDisplayAdapter @Inject constructor( override val size: SizeKM get() = ctx.getRealDisplaySize() + override fun getSupportedResolutions(): List { + val display = displayManager.displays.firstOrNull() ?: return emptyList() + + return display.supportedModes + .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } + .distinct() + } + override val isAmbientDisplayEnabled: MutableStateFlow = MutableStateFlow(isAodEnabled()) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt index 377e148044..63ea6012ed 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt @@ -15,6 +15,12 @@ interface DisplayAdapter { val size: SizeKM val isAmbientDisplayEnabled: Flow + /** + * The distinct resolutions supported by the default display, taken from the + * display's supported modes. The dimensions are in the display's natural orientation. + */ + fun getSupportedResolutions(): List + fun isAutoRotateEnabled(): Boolean fun enableAutoRotate(): KMResult<*> fun disableAutoRotate(): KMResult<*> From 044b7ea32cc811580dbe7a0e230ca736b8135398 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 11:37:57 +0000 Subject: [PATCH 2/3] #2184 refactor: move display resolution constraint state into view model Hoist the bottom sheet's selection/custom/width/height state and the validity and resolved-resolution logic out of the composable and into ChooseConstraintViewModel. The bottom sheet is now a stateless view that renders DisplayResolutionSheetState and forwards user input through view model callbacks. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011GqGRM7xSwYjxKYT6gp66F --- .../constraints/ChooseConstraintViewModel.kt | 116 ++++++++++++++++-- .../DisplayResolutionConstraintBottomSheet.kt | 113 +++++------------ 2 files changed, 140 insertions(+), 89 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index b5e3729ef8..bf5ce3fd13 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -23,6 +23,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel import io.github.sds100.keymapper.base.utils.ui.showDialog import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation +import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.volume.RingerMode @@ -96,6 +97,7 @@ class ChooseConstraintViewModel @Inject constructor( var timeConstraintState: ConstraintData.Time? by mutableStateOf(null) var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null) + private set init { viewModelScope.launch { @@ -114,9 +116,45 @@ class ChooseConstraintViewModel @Inject constructor( } } - fun onDoneConfigDisplayResolutionClick(width: Int, height: Int) { + fun onSelectDisplayResolution(resolution: SizeKM) { + displayResolutionState = displayResolutionState?.copy( + isCustom = false, + selectedResolution = resolution, + ) + } + + fun onSelectCustomDisplayResolution() { + displayResolutionState = displayResolutionState?.copy(isCustom = true) + } + + fun onDisplayResolutionWidthChange(width: String) { + displayResolutionState = displayResolutionState?.copy( + widthText = width.filter(Char::isDigit), + ) + } + + fun onDisplayResolutionHeightChange(height: String) { + displayResolutionState = displayResolutionState?.copy( + heightText = height.filter(Char::isDigit), + ) + } + + fun onDismissDisplayResolution() { + displayResolutionState = null + } + + fun onDoneConfigDisplayResolutionClick() { + val state = displayResolutionState ?: return + + val resolution = state.resolvedResolution ?: return + viewModelScope.launch { - returnResult.emit(ConstraintData.DisplayResolution(width = width, height = height)) + returnResult.emit( + ConstraintData.DisplayResolution( + width = resolution.width, + height = resolution.height, + ), + ) displayResolutionState = null } } @@ -162,12 +200,7 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) ConstraintId.DISPLAY_RESOLUTION -> { - val currentResolution = useCase.getCurrentResolution() - displayResolutionState = DisplayResolutionSheetState( - supportedResolutions = useCase.getSupportedResolutions(), - initialWidth = currentResolution.width, - initialHeight = currentResolution.height, - ) + displayResolutionState = buildDisplayResolutionState() } ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> @@ -313,6 +346,25 @@ class ChooseConstraintViewModel @Inject constructor( } } + private fun buildDisplayResolutionState(): DisplayResolutionSheetState { + val supportedResolutions = useCase.getSupportedResolutions() + val currentResolution = useCase.getCurrentResolution() + + val matchingResolution = supportedResolutions.firstOrNull { + it.matchesIgnoringOrientation(currentResolution) + } + + return DisplayResolutionSheetState( + supportedResolutions = supportedResolutions, + // Show the text fields immediately when there is nothing meaningful to pick + // from or when the current resolution isn't one of the supported modes. + isCustom = supportedResolutions.size <= 1 || matchingResolution == null, + selectedResolution = matchingResolution ?: supportedResolutions.firstOrNull(), + widthText = currentResolution.width.toString(), + heightText = currentResolution.height.toString(), + ) + } + private suspend fun chooseFlashlightLens(): CameraLens? { val items = useCase.getFlashlightLenses().map { lens -> val label = when (lens) { @@ -616,3 +668,51 @@ class ChooseConstraintViewModel @Inject constructor( returnResult.emit(constraintData) } } + +/** + * State for the display resolution bottom sheet. + * + * @param supportedResolutions the resolutions the display reports as supported. + * @param isCustom whether the user is entering a custom resolution instead of picking a chip. + * @param selectedResolution the currently selected supported resolution, if any. + * @param widthText the custom width input. + * @param heightText the custom height input. + */ +data class DisplayResolutionSheetState( + val supportedResolutions: List, + val isCustom: Boolean, + val selectedResolution: SizeKM?, + val widthText: String, + val heightText: String, +) { + private val customWidth: Int? get() = widthText.toIntOrNull() + private val customHeight: Int? get() = heightText.toIntOrNull() + + /** + * The resolution that will be saved, or null when the current input is not valid. + */ + val resolvedResolution: SizeKM? + get() = if (isCustom) { + val width = customWidth + val height = customHeight + + if (width != null && width > 0 && height != null && height > 0) { + SizeKM(width, height) + } else { + null + } + } else { + selectedResolution + } + + val isValid: Boolean get() = resolvedResolution != null +} + +/** + * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 + * are treated as the same resolution. + */ +private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { + return (width == other.width && height == other.height) || + (width == other.height && height == other.width) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt index 8d8c789372..5424ad8c9a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -21,11 +21,7 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -38,19 +34,6 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.launch -/** - * State passed to the display resolution bottom sheet. - * - * @param supportedResolutions the resolutions the display reports as supported. - * @param initialWidth the current real display width, used to prefill the custom fields. - * @param initialHeight the current real display height, used to prefill the custom fields. - */ -data class DisplayResolutionSheetState( - val supportedResolutions: List, - val initialWidth: Int, - val initialHeight: Int, -) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { @@ -60,57 +43,29 @@ fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) DisplayResolutionConstraintBottomSheet( sheetState = sheetState, state = state, - onDismissRequest = { viewModel.displayResolutionState = null }, - onDoneClick = { width, height -> - viewModel.onDoneConfigDisplayResolutionClick(width, height) - }, + onSelectResolution = viewModel::onSelectDisplayResolution, + onSelectCustom = viewModel::onSelectCustomDisplayResolution, + onWidthChange = viewModel::onDisplayResolutionWidthChange, + onHeightChange = viewModel::onDisplayResolutionHeightChange, + onDismissRequest = viewModel::onDismissDisplayResolution, + onDoneClick = viewModel::onDoneConfigDisplayResolutionClick, ) } -/** - * Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080 - * are treated as the same resolution. - */ -private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean { - return (width == other.width && height == other.height) || - (width == other.height && height == other.width) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DisplayResolutionConstraintBottomSheet( sheetState: SheetState, state: DisplayResolutionSheetState, + onSelectResolution: (SizeKM) -> Unit = {}, + onSelectCustom: () -> Unit = {}, + onWidthChange: (String) -> Unit = {}, + onHeightChange: (String) -> Unit = {}, onDismissRequest: () -> Unit = {}, - onDoneClick: (width: Int, height: Int) -> Unit = { _, _ -> }, + onDoneClick: () -> Unit = {}, ) { val scope = rememberCoroutineScope() - val initialSize = remember(state) { SizeKM(state.initialWidth, state.initialHeight) } - val matchingResolution = remember(state) { - state.supportedResolutions.firstOrNull { it.matchesIgnoringOrientation(initialSize) } - } - - // Show the text fields immediately when there is nothing meaningful to pick from - // or when the current resolution isn't one of the supported modes. - var isCustom by remember(state) { - mutableStateOf(state.supportedResolutions.size <= 1 || matchingResolution == null) - } - var selectedResolution by remember(state) { - mutableStateOf(matchingResolution ?: state.supportedResolutions.firstOrNull()) - } - var widthText by remember(state) { mutableStateOf(state.initialWidth.toString()) } - var heightText by remember(state) { mutableStateOf(state.initialHeight.toString()) } - - val customWidth = widthText.toIntOrNull() - val customHeight = heightText.toIntOrNull() - - val isValid = if (isCustom) { - customWidth != null && customWidth > 0 && customHeight != null && customHeight > 0 - } else { - selectedResolution != null - } - ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, @@ -140,18 +95,15 @@ private fun DisplayResolutionConstraintBottomSheet( ) { for (resolution in state.supportedResolutions) { FilterChip( - selected = !isCustom && selectedResolution == resolution, - onClick = { - isCustom = false - selectedResolution = resolution - }, + selected = !state.isCustom && state.selectedResolution == resolution, + onClick = { onSelectResolution(resolution) }, label = { Text("${resolution.width} × ${resolution.height}") }, ) } FilterChip( - selected = isCustom, - onClick = { isCustom = true }, + selected = state.isCustom, + onClick = onSelectCustom, label = { Text( stringResource( @@ -165,7 +117,7 @@ private fun DisplayResolutionConstraintBottomSheet( Spacer(modifier = Modifier.height(16.dp)) } - if (isCustom) { + if (state.isCustom) { Row( modifier = Modifier .fillMaxWidth() @@ -175,8 +127,8 @@ private fun DisplayResolutionConstraintBottomSheet( ) { OutlinedTextField( modifier = Modifier.weight(1f), - value = widthText, - onValueChange = { widthText = it.filter(Char::isDigit) }, + value = state.widthText, + onValueChange = onWidthChange, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { @@ -186,8 +138,8 @@ private fun DisplayResolutionConstraintBottomSheet( OutlinedTextField( modifier = Modifier.weight(1f), - value = heightText, - onValueChange = { heightText = it.filter(Char::isDigit) }, + value = state.heightText, + onValueChange = onHeightChange, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { @@ -222,16 +174,11 @@ private fun DisplayResolutionConstraintBottomSheet( Button( modifier = Modifier.weight(1f), - enabled = isValid, + enabled = state.isValid, onClick = { - val width = if (isCustom) customWidth else selectedResolution?.width - val height = if (isCustom) customHeight else selectedResolution?.height - - if (width != null && height != null) { - scope.launch { - sheetState.hide() - onDoneClick(width, height) - } + scope.launch { + sheetState.hide() + onDoneClick() } }, ) { @@ -262,8 +209,10 @@ private fun PreviewWithModes() { SizeKM(1080, 2400), SizeKM(1440, 3200), ), - initialWidth = 1080, - initialHeight = 2400, + isCustom = false, + selectedResolution = SizeKM(1080, 2400), + widthText = "1080", + heightText = "2400", ), ) } @@ -284,8 +233,10 @@ private fun PreviewCustomOnly() { sheetState = sheetState, state = DisplayResolutionSheetState( supportedResolutions = emptyList(), - initialWidth = 1080, - initialHeight = 2400, + isCustom = true, + selectedResolution = null, + widthText = "1080", + heightText = "2400", ), ) } From 2514706a0849121dde6bd466803e3a2ed6331ffd Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 12 Jul 2026 21:51:29 +0200 Subject: [PATCH 3/3] #2184 listen to changes in display resolutions --- .../constraints/ChooseConstraintViewModel.kt | 11 +- .../base/constraints/ConstraintDependency.kt | 1 + .../base/constraints/ConstraintUtils.kt | 154 ++++++++++++++++++ .../constraints/CreateConstraintUseCase.kt | 6 +- .../constraints/DetectConstraintsUseCase.kt | 17 ++ .../accessibility/BaseAccessibilityService.kt | 2 + .../system/display/AndroidDisplayAdapter.kt | 35 ++-- .../system/display/DisplayAdapter.kt | 3 +- 8 files changed, 213 insertions(+), 16 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index bf5ce3fd13..2367aa378f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -173,6 +173,7 @@ class ChooseConstraintViewModel @Inject constructor( onSelectDisplayOrientationConstraint() return@launch } + PHYSICAL_ORIENTATION_LIST_ITEM_ID -> { onSelectPhysicalOrientationConstraint() return@launch @@ -187,6 +188,7 @@ class ChooseConstraintViewModel @Inject constructor( -> onSelectAppConstraint(constraintType) ConstraintId.MEDIA_PLAYING -> returnResult.emit(ConstraintData.MediaPlaying) + ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(ConstraintData.NoMediaPlaying) ConstraintId.BT_DEVICE_CONNECTED, @@ -268,6 +270,7 @@ class ChooseConstraintViewModel @Inject constructor( } ConstraintId.WIFI_ON -> returnResult.emit(ConstraintData.WifiOn) + ConstraintId.WIFI_OFF -> returnResult.emit(ConstraintData.WifiOff) ConstraintId.WIFI_CONNECTED, @@ -355,7 +358,7 @@ class ChooseConstraintViewModel @Inject constructor( } return DisplayResolutionSheetState( - supportedResolutions = supportedResolutions, + supportedResolutions = supportedResolutions.sortedBy { it.width }, // Show the text fields immediately when there is nothing meaningful to pick // from or when the current resolution isn't one of the supported modes. isCustom = supportedResolutions.size <= 1 || matchingResolution == null, @@ -406,15 +409,21 @@ class ChooseConstraintViewModel @Inject constructor( val constraintData = when (selectedOrientation) { ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape + ConstraintId.DISPLAY_ORIENTATION_0 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0) + ConstraintId.DISPLAY_ORIENTATION_90 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90) + ConstraintId.DISPLAY_ORIENTATION_180 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180) + ConstraintId.DISPLAY_ORIENTATION_270 -> ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270) + else -> return } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt index 454a3324e9..ac326d39b4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt @@ -20,4 +20,5 @@ enum class ConstraintDependency { CHARGING_STATE, HINGE_STATE, NOTIFICATION_PANEL_STATE, + DISPLAY_RESOLUTIONS, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 6fae3f13f3..cb9d2f2f79 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -134,11 +134,13 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Rounded.Android) ConstraintId.MEDIA_PLAYING -> ComposeIconInfo.Vector(Icons.Outlined.PlayArrow) + ConstraintId.MEDIA_NOT_PLAYING -> ComposeIconInfo.Vector(Icons.Outlined.StopCircle) ConstraintId.BT_DEVICE_CONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.BluetoothConnected, ) + ConstraintId.BT_DEVICE_DISCONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.BluetoothDisabled, ) @@ -154,6 +156,7 @@ object ConstraintUtils { ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentLandscape, ) + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentPortrait, ) @@ -167,18 +170,23 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff) + ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) ConstraintId.DISPLAY_RESOLUTION -> ComposeIconInfo.Vector(Icons.Outlined.AspectRatio) ConstraintId.FLASHLIGHT_OFF -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOff) + ConstraintId.FLASHLIGHT_ON -> ComposeIconInfo.Vector(Icons.Outlined.FlashlightOn) ConstraintId.WIFI_CONNECTED -> ComposeIconInfo.Vector(Icons.Outlined.Wifi) + ConstraintId.WIFI_DISCONNECTED -> ComposeIconInfo.Vector( Icons.Outlined.SignalWifiStatusbarNull, ) + ConstraintId.WIFI_OFF -> ComposeIconInfo.Vector(Icons.Outlined.WifiOff) + ConstraintId.WIFI_ON -> ComposeIconInfo.Vector(Icons.Outlined.Wifi) ConstraintId.IME_CHOSEN, @@ -186,31 +194,42 @@ object ConstraintUtils { -> ComposeIconInfo.Vector(Icons.Outlined.Keyboard) ConstraintId.KEYBOARD_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.Keyboard) + ConstraintId.KEYBOARD_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.KeyboardHide) ConstraintId.DEVICE_IS_LOCKED -> ComposeIconInfo.Vector(Icons.Outlined.Lock) + ConstraintId.DEVICE_IS_UNLOCKED -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) ConstraintId.IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.Call) + ConstraintId.NOT_IN_PHONE_CALL -> ComposeIconInfo.Vector(Icons.Outlined.CallEnd) + ConstraintId.PHONE_RINGING -> ComposeIconInfo.Vector(Icons.Outlined.RingVolume) + ConstraintId.RINGER_MODE_NORMAL -> ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.RINGER_MODE_VIBRATE -> ComposeIconInfo.Vector(Icons.Outlined.Vibration) + ConstraintId.RINGER_MODE_SILENT -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) ConstraintId.CHARGING -> ComposeIconInfo.Vector(Icons.Outlined.BatteryChargingFull) + ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar) ConstraintId.HINGE_CLOSED -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + ConstraintId.HINGE_OPEN -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) ConstraintId.LOCK_SCREEN_SHOWING -> ComposeIconInfo.Vector( Icons.Outlined.ScreenLockPortrait, ) + ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) ConstraintId.NOTIFICATION_PANEL_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.Notifications) + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.NotificationsOff) @@ -219,60 +238,195 @@ object ConstraintUtils { fun getTitleStringId(constraintId: ConstraintId): Int = when (constraintId) { ConstraintId.APP_IN_FOREGROUND -> R.string.constraint_choose_app_foreground + ConstraintId.APP_NOT_IN_FOREGROUND -> R.string.constraint_choose_app_not_foreground + ConstraintId.APP_PLAYING_MEDIA -> R.string.constraint_choose_app_playing_media + ConstraintId.APP_NOT_PLAYING_MEDIA -> R.string.constraint_choose_app_not_playing_media + ConstraintId.MEDIA_NOT_PLAYING -> R.string.constraint_choose_media_not_playing + ConstraintId.MEDIA_PLAYING -> R.string.constraint_choose_media_playing + ConstraintId.BT_DEVICE_CONNECTED -> R.string.constraint_choose_bluetooth_device_connected + ConstraintId.BT_DEVICE_DISCONNECTED -> R.string.constraint_choose_bluetooth_device_disconnected + ConstraintId.SCREEN_ON -> R.string.constraint_choose_screen_on_description + ConstraintId.SCREEN_OFF -> R.string.constraint_choose_screen_off_description + ConstraintId.DISPLAY_RESOLUTION -> R.string.constraint_choose_display_resolution + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape + ConstraintId.DISPLAY_ORIENTATION_0 -> R.string.constraint_choose_orientation_0 + ConstraintId.DISPLAY_ORIENTATION_90 -> R.string.constraint_choose_orientation_90 + ConstraintId.DISPLAY_ORIENTATION_180 -> R.string.constraint_choose_orientation_180 + ConstraintId.DISPLAY_ORIENTATION_270 -> R.string.constraint_choose_orientation_270 + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT -> R.string.constraint_choose_physical_orientation_portrait + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE -> R.string.constraint_choose_physical_orientation_landscape + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED -> R.string.constraint_choose_physical_orientation_portrait_inverted + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> R.string.constraint_choose_physical_orientation_landscape_inverted + ConstraintId.FLASHLIGHT_ON -> R.string.constraint_flashlight_on + ConstraintId.FLASHLIGHT_OFF -> R.string.constraint_flashlight_off + ConstraintId.WIFI_ON -> R.string.constraint_wifi_on + ConstraintId.WIFI_OFF -> R.string.constraint_wifi_off + ConstraintId.WIFI_CONNECTED -> R.string.constraint_wifi_connected + ConstraintId.WIFI_DISCONNECTED -> R.string.constraint_wifi_disconnected + ConstraintId.IME_CHOSEN -> R.string.constraint_ime_chosen + ConstraintId.IME_NOT_CHOSEN -> R.string.constraint_ime_not_chosen + ConstraintId.KEYBOARD_SHOWING -> R.string.constraint_keyboard_showing + ConstraintId.KEYBOARD_NOT_SHOWING -> R.string.constraint_keyboard_not_showing + ConstraintId.DEVICE_IS_LOCKED -> R.string.constraint_device_is_locked + ConstraintId.DEVICE_IS_UNLOCKED -> R.string.constraint_device_is_unlocked + ConstraintId.IN_PHONE_CALL -> R.string.constraint_in_phone_call + ConstraintId.NOT_IN_PHONE_CALL -> R.string.constraint_not_in_phone_call + ConstraintId.PHONE_RINGING -> R.string.constraint_phone_ringing + ConstraintId.RINGER_MODE_NORMAL -> R.string.constraint_ringer_mode_normal + ConstraintId.RINGER_MODE_VIBRATE -> R.string.constraint_ringer_mode_vibrate + ConstraintId.RINGER_MODE_SILENT -> R.string.constraint_ringer_mode_silent + ConstraintId.CHARGING -> R.string.constraint_charging + ConstraintId.DISCHARGING -> R.string.constraint_discharging + ConstraintId.HINGE_CLOSED -> R.string.constraint_hinge_closed + ConstraintId.HINGE_OPEN -> R.string.constraint_hinge_open + ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing + ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing + ConstraintId.NOTIFICATION_PANEL_SHOWING -> R.string.constraint_notification_panel_showing + ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING -> R.string.constraint_notification_panel_not_showing + ConstraintId.TIME -> R.string.constraint_time } + + fun Constraint.getDependency(): Set { + return when (data) { + is ConstraintData.AppInForeground, + is ConstraintData.AppNotInForeground, + -> setOf(ConstraintDependency.FOREGROUND_APP) + + is ConstraintData.AppNotPlayingMedia, + is ConstraintData.AppPlayingMedia, + -> setOf(ConstraintDependency.APP_PLAYING_MEDIA) + + is ConstraintData.BtDeviceConnected, + is ConstraintData.BtDeviceDisconnected, + -> setOf(ConstraintDependency.CONNECTED_BT_DEVICES) + + is ConstraintData.Charging, + is ConstraintData.Discharging, + -> setOf(ConstraintDependency.CHARGING_STATE) + + is ConstraintData.DeviceIsLocked, + is ConstraintData.DeviceIsUnlocked, + -> setOf(ConstraintDependency.DEVICE_LOCKED_STATE) + + is ConstraintData.FlashlightOff, + is ConstraintData.FlashlightOn, + -> setOf(ConstraintDependency.FLASHLIGHT_STATE) + + is ConstraintData.ImeChosen, + is ConstraintData.ImeNotChosen, + -> setOf(ConstraintDependency.CHOSEN_IME) + + is ConstraintData.InPhoneCall, + is ConstraintData.NotInPhoneCall, + is ConstraintData.PhoneRinging, + -> setOf(ConstraintDependency.PHONE_STATE) + + is ConstraintData.MediaPlaying, + is ConstraintData.NoMediaPlaying, + -> setOf(ConstraintDependency.MEDIA_PLAYING) + + is ConstraintData.OrientationCustom, + is ConstraintData.OrientationLandscape, + is ConstraintData.OrientationPortrait, + -> setOf(ConstraintDependency.DISPLAY_ORIENTATION) + + is ConstraintData.ScreenOff, + is ConstraintData.ScreenOn, + -> setOf(ConstraintDependency.SCREEN_STATE) + + is ConstraintData.WifiConnected, + is ConstraintData.WifiDisconnected, + -> setOf(ConstraintDependency.WIFI_SSID) + + is ConstraintData.WifiOff, + is ConstraintData.WifiOn, + -> setOf(ConstraintDependency.WIFI_STATE) + + is ConstraintData.LockScreenShowing, + is ConstraintData.LockScreenNotShowing, + -> setOf(ConstraintDependency.LOCK_SCREEN_SHOWING) + + is ConstraintData.Time -> setOf( + ConstraintDependency.FOREGROUND_APP, + ConstraintDependency.SCREEN_STATE, + ) + + is ConstraintData.HingeClosed -> setOf(ConstraintDependency.HINGE_STATE) + + is ConstraintData.HingeOpen -> setOf(ConstraintDependency.HINGE_STATE) + + ConstraintData.KeyboardNotShowing, + ConstraintData.KeyboardShowing, + -> setOf(ConstraintDependency.KEYBOARD_VISIBLE) + + is ConstraintData.PhysicalOrientation -> setOf( + ConstraintDependency.PHYSICAL_ORIENTATION, + ) + + is ConstraintData.RingerMode -> setOf(ConstraintDependency.RINGER_MODE) + + ConstraintData.NotificationPanelNotShowing, ConstraintData.NotificationPanelShowing -> + setOf( + ConstraintDependency.NOTIFICATION_PANEL_STATE, + ) + + is ConstraintData.DisplayResolution -> setOf(ConstraintDependency.DISPLAY_RESOLUTIONS) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt index aa83d41020..a9650ccbd5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt @@ -34,11 +34,13 @@ class CreateConstraintUseCaseImpl @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_CAMERA_FLASH) } } + ConstraintId.HINGE_CLOSED, ConstraintId.HINGE_OPEN -> { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return KMError.SdkVersionTooLow(Build.VERSION_CODES.R) } } + else -> Unit } @@ -78,7 +80,7 @@ class CreateConstraintUseCaseImpl @Inject constructor( return CameraLens.entries.filter { cameraAdapter.getFlashInfo(it) != null }.toSet() } - override fun getSupportedResolutions(): List = displayAdapter.getSupportedResolutions() + override fun getSupportedResolutions(): Set = displayAdapter.supportedResolutions.value override fun getCurrentResolution(): SizeKM = displayAdapter.size } @@ -93,6 +95,6 @@ interface CreateConstraintUseCase { fun getFlashlightLenses(): Set - fun getSupportedResolutions(): List + fun getSupportedResolutions(): Set fun getCurrentResolution(): SizeKM } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt index 14e2da8297..a2eb148de6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt @@ -63,6 +63,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.FOREGROUND_APP -> accessibilityService.activeWindowPackage.map { dependency } + ConstraintDependency.APP_PLAYING_MEDIA, ConstraintDependency.MEDIA_PLAYING -> merge( mediaAdapter.getActiveMediaSessionPackagesFlow(), @@ -71,11 +72,15 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.CONNECTED_BT_DEVICES -> devicesAdapter.connectedBluetoothDevices.map { dependency } + ConstraintDependency.SCREEN_STATE -> displayAdapter.isScreenOn.map { dependency } + ConstraintDependency.DISPLAY_ORIENTATION -> displayAdapter.orientation.map { dependency } + ConstraintDependency.PHYSICAL_ORIENTATION -> displayAdapter.physicalOrientation.map { dependency } + ConstraintDependency.FLASHLIGHT_STATE -> merge( cameraAdapter.isFlashlightOnFlow(CameraLens.FRONT), cameraAdapter.isFlashlightOnFlow(CameraLens.BACK), @@ -83,8 +88,11 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.WIFI_SSID -> networkAdapter.connectedWifiSSIDFlow.map { dependency } + ConstraintDependency.WIFI_STATE -> networkAdapter.isWifiEnabledFlow().map { dependency } + ConstraintDependency.CHOSEN_IME -> inputMethodAdapter.chosenIme.map { dependency } + ConstraintDependency.DEVICE_LOCKED_STATE -> lockScreenAdapter.isLockedFlow().map { dependency } @@ -95,18 +103,27 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ).map { dependency } ConstraintDependency.PHONE_STATE -> phoneAdapter.callStateFlow.map { dependency } + ConstraintDependency.RINGER_MODE -> volumeAdapter.ringerModeFlow.map { dependency } + ConstraintDependency.CHARGING_STATE -> powerAdapter.isCharging.map { dependency } + ConstraintDependency.HINGE_STATE -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { foldableAdapter.hingeState.map { dependency } } else { emptyFlow() } + ConstraintDependency.KEYBOARD_VISIBLE -> accessibilityService.isInputMethodVisible.map { dependency } + ConstraintDependency.NOTIFICATION_PANEL_STATE -> accessibilityService.isNotificationShadeExpanded.map { dependency } + + ConstraintDependency.DISPLAY_RESOLUTIONS -> displayAdapter.supportedResolutions.map { + dependency + } } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index 8e1af05ff5..1eda79100b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -294,6 +294,8 @@ abstract class BaseAccessibilityService : override fun onAccessibilityEvent(event: AccessibilityEvent?) { event ?: return + Timber.i("On Accessibility event: $event") + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { // Catch exceptions because there is a crash report where // getRootInActivityWindow() fails internally inside the AccessibilityService. diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 1c3a1d0e12..48867bcfda 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -101,17 +101,12 @@ class AndroidDisplayAdapter @Inject constructor( override val size: SizeKM get() = ctx.getRealDisplaySize() - override fun getSupportedResolutions(): List { - val display = displayManager.displays.firstOrNull() ?: return emptyList() - - return display.supportedModes - .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } - .distinct() - } - override val isAmbientDisplayEnabled: MutableStateFlow = MutableStateFlow(isAodEnabled()) + override val supportedResolutions: MutableStateFlow> = + MutableStateFlow(getSupportedResolutions()) + private val orientationEventListener = object : OrientationEventListener(ctx) { override fun onOrientationChanged(orientationDegrees: Int) { if (orientationDegrees == ORIENTATION_UNKNOWN) { @@ -127,15 +122,15 @@ class AndroidDisplayAdapter @Inject constructor( displayManager.registerDisplayListener( object : DisplayManager.DisplayListener { override fun onDisplayAdded(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() } override fun onDisplayRemoved(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() } override fun onDisplayChanged(displayId: Int) { - _orientation.update { getDisplayOrientation() } + onDisplaysChanged() // This listener API has lower latency than the broadcast receiver so also use this. val isScreenStateOn = displayManager.displays.first().state == Display.STATE_ON @@ -291,7 +286,6 @@ class AndroidDisplayAdapter @Inject constructor( Surface.ROTATION_90 -> Orientation.ORIENTATION_90 Surface.ROTATION_180 -> Orientation.ORIENTATION_180 Surface.ROTATION_270 -> Orientation.ORIENTATION_270 - else -> throw Exception("Don't know how to convert $sdkRotation to Orientation") } @@ -299,6 +293,11 @@ class AndroidDisplayAdapter @Inject constructor( return SettingsUtils.getSecureSetting(ctx, "doze_always_on") == 1 } + private fun onDisplaysChanged() { + _orientation.update { getDisplayOrientation() } + supportedResolutions.update { getSupportedResolutions() } + } + /** * Converts sensor orientation degrees to PhysicalOrientation. * @@ -321,13 +320,25 @@ class AndroidDisplayAdapter @Inject constructor( degrees >= (360 - ORIENTATION_TOLERANCE) || degrees < ORIENTATION_TOLERANCE -> PhysicalOrientation.PORTRAIT + degrees in (90 - ORIENTATION_TOLERANCE) until (90 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.LANDSCAPE + degrees in (180 - ORIENTATION_TOLERANCE) until (180 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.PORTRAIT_INVERTED + degrees in (270 - ORIENTATION_TOLERANCE) until (270 + ORIENTATION_TOLERANCE) -> PhysicalOrientation.LANDSCAPE_INVERTED + else -> _physicalOrientation.value // Keep current orientation in transition zone } } + + private fun getSupportedResolutions(): Set { + val display = displayManager.displays.firstOrNull() ?: return emptySet() + + return display.supportedModes + .map { mode -> SizeKM(mode.physicalWidth, mode.physicalHeight) } + .toSet() + } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt index 63ea6012ed..cec0291d52 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayAdapter.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.SizeKM import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface DisplayAdapter { val isScreenOn: Flow @@ -19,7 +20,7 @@ interface DisplayAdapter { * The distinct resolutions supported by the default display, taken from the * display's supported modes. The dimensions are in the display's natural orientation. */ - fun getSupportedResolutions(): List + val supportedResolutions: StateFlow> fun isAutoRotateEnabled(): Boolean fun enableAutoRotate(): KMResult<*>