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..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 @@ -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 @@ -95,6 +96,9 @@ class ChooseConstraintViewModel @Inject constructor( var timeConstraintState: ConstraintData.Time? by mutableStateOf(null) + var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null) + private set + init { viewModelScope.launch { returnResult.collect { constraintData -> @@ -112,6 +116,49 @@ class ChooseConstraintViewModel @Inject constructor( } } + 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 = resolution.width, + height = resolution.height, + ), + ) + displayResolutionState = null + } + } + fun onNavigateBack() { viewModelScope.launch { popBackStack() @@ -126,6 +173,7 @@ class ChooseConstraintViewModel @Inject constructor( onSelectDisplayOrientationConstraint() return@launch } + PHYSICAL_ORIENTATION_LIST_ITEM_ID -> { onSelectPhysicalOrientationConstraint() return@launch @@ -140,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, @@ -152,6 +201,10 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) + ConstraintId.DISPLAY_RESOLUTION -> { + displayResolutionState = buildDisplayResolutionState() + } + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> returnResult.emit(ConstraintData.OrientationPortrait) @@ -217,6 +270,7 @@ class ChooseConstraintViewModel @Inject constructor( } ConstraintId.WIFI_ON -> returnResult.emit(ConstraintData.WifiOn) + ConstraintId.WIFI_OFF -> returnResult.emit(ConstraintData.WifiOff) ConstraintId.WIFI_CONNECTED, @@ -295,6 +349,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.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, + 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) { @@ -336,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 } @@ -598,3 +677,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/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/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/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..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 @@ -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, @@ -132,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, ) @@ -152,6 +156,7 @@ object ConstraintUtils { ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentLandscape, ) + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentPortrait, ) @@ -165,16 +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, @@ -182,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) @@ -215,59 +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 93b1d72b9d..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 @@ -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? { @@ -31,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 } @@ -74,6 +79,10 @@ class CreateConstraintUseCaseImpl @Inject constructor( override fun getFlashlightLenses(): Set { return CameraLens.entries.filter { cameraAdapter.getFlashInfo(it) != null }.toSet() } + + override fun getSupportedResolutions(): Set = displayAdapter.supportedResolutions.value + + override fun getCurrentResolution(): SizeKM = displayAdapter.size } interface CreateConstraintUseCase { @@ -85,4 +94,7 @@ interface CreateConstraintUseCase { fun getSavedWifiSSIDs(): Flow> fun getFlashlightLenses(): Set + + 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/constraints/DisplayResolutionConstraintBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt new file mode 100644 index 0000000000..5424ad8c9a --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DisplayResolutionConstraintBottomSheet.kt @@ -0,0 +1,243 @@ +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.rememberCoroutineScope +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DisplayResolutionConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val state = viewModel.displayResolutionState ?: return + + DisplayResolutionConstraintBottomSheet( + sheetState = sheetState, + state = state, + onSelectResolution = viewModel::onSelectDisplayResolution, + onSelectCustom = viewModel::onSelectCustomDisplayResolution, + onWidthChange = viewModel::onDisplayResolutionWidthChange, + onHeightChange = viewModel::onDisplayResolutionHeightChange, + onDismissRequest = viewModel::onDismissDisplayResolution, + onDoneClick = viewModel::onDoneConfigDisplayResolutionClick, + ) +} + +@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: () -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + 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 = !state.isCustom && state.selectedResolution == resolution, + onClick = { onSelectResolution(resolution) }, + label = { Text("${resolution.width} × ${resolution.height}") }, + ) + } + + FilterChip( + selected = state.isCustom, + onClick = onSelectCustom, + label = { + Text( + stringResource( + R.string.constraint_display_resolution_custom_chip, + ), + ) + }, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + + if (state.isCustom) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedTextField( + modifier = Modifier.weight(1f), + value = state.widthText, + onValueChange = onWidthChange, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { + Text(stringResource(R.string.constraint_display_resolution_width)) + }, + ) + + OutlinedTextField( + modifier = Modifier.weight(1f), + value = state.heightText, + onValueChange = onHeightChange, + 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 = state.isValid, + onClick = { + scope.launch { + sheetState.hide() + onDoneClick() + } + }, + ) { + 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), + ), + isCustom = false, + selectedResolution = SizeKM(1080, 2400), + widthText = "1080", + heightText = "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(), + isCustom = true, + selectedResolution = null, + widthText = "1080", + heightText = "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/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/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..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 @@ -104,6 +104,9 @@ class AndroidDisplayAdapter @Inject constructor( 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) { @@ -119,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 @@ -283,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") } @@ -291,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. * @@ -313,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 377e148044..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 @@ -15,6 +16,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. + */ + val supportedResolutions: StateFlow> + fun isAutoRotateEnabled(): Boolean fun enableAutoRotate(): KMResult<*> fun disableAutoRotate(): KMResult<*>