diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a87dd241..cb69c38418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #1915 ask user to remove "adb shell" from Shell command. - #1904 inform the user how to enable the accessibility service with PRO mode or ADB. +- #1911 constraint for physical device orientation that ignores auto rotate setting. ## Bug fixes 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 1b24b89c2d..836834e723 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 @@ -1,5 +1,7 @@ package io.github.sds100.keymapper.base.constraints +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.StayCurrentPortrait import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -15,10 +17,12 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemGroup 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.State import io.github.sds100.keymapper.system.camera.CameraLens import javax.inject.Inject @@ -46,6 +50,10 @@ class ChooseConstraintViewModel @Inject constructor( NavigationProvider by navigationProvider { companion object { + // Synthetic IDs for consolidated orientation list items (not actual ConstraintIds) + private const val DISPLAY_ORIENTATION_LIST_ITEM_ID = "display_orientation" + private const val PHYSICAL_ORIENTATION_LIST_ITEM_ID = "physical_orientation" + private val CATEGORY_ORDER = arrayOf( ConstraintCategory.APPS, ConstraintCategory.MEDIA, @@ -111,6 +119,18 @@ class ChooseConstraintViewModel @Inject constructor( fun onListItemClick(id: String) { viewModelScope.launch { + // Handle synthetic list item IDs for consolidated orientation constraints + when (id) { + DISPLAY_ORIENTATION_LIST_ITEM_ID -> { + onSelectDisplayOrientationConstraint() + return@launch + } + PHYSICAL_ORIENTATION_LIST_ITEM_ID -> { + onSelectPhysicalOrientationConstraint() + return@launch + } + } + when (val constraintType = ConstraintId.valueOf(id)) { ConstraintId.APP_IN_FOREGROUND, ConstraintId.APP_NOT_IN_FOREGROUND, @@ -131,32 +151,60 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff) - ConstraintId.ORIENTATION_PORTRAIT -> + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> returnResult.emit(ConstraintData.OrientationPortrait) - ConstraintId.ORIENTATION_LANDSCAPE -> + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> returnResult.emit(ConstraintData.OrientationLandscape) - ConstraintId.ORIENTATION_0 -> + ConstraintId.DISPLAY_ORIENTATION_0 -> returnResult.emit( ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0), ) - ConstraintId.ORIENTATION_90 -> + ConstraintId.DISPLAY_ORIENTATION_90 -> returnResult.emit( ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90), ) - ConstraintId.ORIENTATION_180 -> + ConstraintId.DISPLAY_ORIENTATION_180 -> returnResult.emit( ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180), ) - ConstraintId.ORIENTATION_270 -> + ConstraintId.DISPLAY_ORIENTATION_270 -> returnResult.emit( ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270), ) + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT -> + returnResult.emit( + ConstraintData.PhysicalOrientation( + physicalOrientation = PhysicalOrientation.PORTRAIT, + ), + ) + + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE -> + returnResult.emit( + ConstraintData.PhysicalOrientation( + physicalOrientation = PhysicalOrientation.LANDSCAPE, + ), + ) + + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED -> + returnResult.emit( + ConstraintData.PhysicalOrientation( + physicalOrientation = PhysicalOrientation.PORTRAIT_INVERTED, + ), + ) + + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> + returnResult.emit( + ConstraintData.PhysicalOrientation( + physicalOrientation = PhysicalOrientation.LANDSCAPE_INVERTED, + ), + ) + ConstraintId.FLASHLIGHT_ON -> { val lens = chooseFlashlightLens() ?: return@launch returnResult.emit(ConstraintData.FlashlightOn(lens = lens)) @@ -251,18 +299,117 @@ class ChooseConstraintViewModel @Inject constructor( return cameraLens } + private suspend fun onSelectDisplayOrientationConstraint() { + val items = listOf( + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT to + getString(R.string.constraint_choose_orientation_portrait), + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE to + getString(R.string.constraint_choose_orientation_landscape), + ConstraintId.DISPLAY_ORIENTATION_0 to + getString(R.string.constraint_choose_orientation_0), + ConstraintId.DISPLAY_ORIENTATION_90 to + getString(R.string.constraint_choose_orientation_90), + ConstraintId.DISPLAY_ORIENTATION_180 to + getString(R.string.constraint_choose_orientation_180), + ConstraintId.DISPLAY_ORIENTATION_270 to + getString(R.string.constraint_choose_orientation_270), + ) + + val dialog = DialogModel.SingleChoice(items) + val selectedOrientation = showDialog("choose_display_orientation", dialog) ?: return + + 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 + } + + returnResult.emit(constraintData) + } + + private suspend fun onSelectPhysicalOrientationConstraint() { + val items = listOf( + PhysicalOrientation.PORTRAIT to + getString(R.string.constraint_choose_physical_orientation_portrait), + PhysicalOrientation.LANDSCAPE to + getString(R.string.constraint_choose_physical_orientation_landscape), + PhysicalOrientation.PORTRAIT_INVERTED to + getString(R.string.constraint_choose_physical_orientation_portrait_inverted), + PhysicalOrientation.LANDSCAPE_INVERTED to + getString(R.string.constraint_choose_physical_orientation_landscape_inverted), + ) + + val dialog = DialogModel.SingleChoice(items) + val selectedOrientation = showDialog("choose_physical_orientation", dialog) ?: return + + returnResult.emit( + ConstraintData.PhysicalOrientation(physicalOrientation = selectedOrientation), + ) + } + private fun buildListGroups(): List = buildList { - val listItems = buildListItems(ConstraintId.entries) + // Filter out individual orientation constraints - show only the consolidated ones + val filteredConstraints = ConstraintId.entries.filter { constraintId -> + constraintId !in listOf( + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT, + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE, + ConstraintId.DISPLAY_ORIENTATION_0, + ConstraintId.DISPLAY_ORIENTATION_90, + ConstraintId.DISPLAY_ORIENTATION_180, + ConstraintId.DISPLAY_ORIENTATION_270, + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT, + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE, + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + ) + } + + val listItems = buildListItems(filteredConstraints) + + // Add synthetic orientation list items + val displayOrientationItem = SimpleListItemModel( + id = DISPLAY_ORIENTATION_LIST_ITEM_ID, + title = getString(R.string.constraint_choose_screen_orientation), + icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait), + isEnabled = true, + ) + + val physicalOrientationItem = SimpleListItemModel( + id = PHYSICAL_ORIENTATION_LIST_ITEM_ID, + title = getString(R.string.constraint_choose_physical_orientation), + icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait), + isEnabled = true, + ) for (category in CATEGORY_ORDER) { val header = getString(ConstraintUtils.getCategoryLabel(category)) + val categoryItems = listItems.filter { item -> + item.isEnabled && + try { + ConstraintUtils.getCategory(ConstraintId.valueOf(item.id)) == category + } catch (e: IllegalArgumentException) { + false + } + }.toMutableList() + + // Add synthetic orientation items to DISPLAY category + if (category == ConstraintCategory.DISPLAY) { + categoryItems.add(displayOrientationItem) + categoryItems.add(physicalOrientationItem) + } + val group = SimpleListItemGroup( header, - items = listItems.filter { - it.isEnabled && - ConstraintUtils.getCategory(ConstraintId.valueOf(it.id)) == category - }, + items = categoryItems, ) if (group.items.isNotEmpty()) { 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 547802579f..9b7f91573d 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 @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.constraints import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.getKey import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.data.entities.ConstraintEntity @@ -69,21 +70,33 @@ sealed class ConstraintData { @Serializable data object OrientationPortrait : ConstraintData() { - override val id: ConstraintId = ConstraintId.ORIENTATION_PORTRAIT + override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_PORTRAIT } @Serializable data object OrientationLandscape : ConstraintData() { - override val id: ConstraintId = ConstraintId.ORIENTATION_LANDSCAPE + override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE } @Serializable data class OrientationCustom(val orientation: Orientation) : ConstraintData() { override val id: ConstraintId = when (orientation) { - Orientation.ORIENTATION_0 -> ConstraintId.ORIENTATION_0 - Orientation.ORIENTATION_90 -> ConstraintId.ORIENTATION_90 - Orientation.ORIENTATION_180 -> ConstraintId.ORIENTATION_180 - Orientation.ORIENTATION_270 -> ConstraintId.ORIENTATION_270 + Orientation.ORIENTATION_0 -> ConstraintId.DISPLAY_ORIENTATION_0 + Orientation.ORIENTATION_90 -> ConstraintId.DISPLAY_ORIENTATION_90 + Orientation.ORIENTATION_180 -> ConstraintId.DISPLAY_ORIENTATION_180 + Orientation.ORIENTATION_270 -> ConstraintId.DISPLAY_ORIENTATION_270 + } + } + + @Serializable + data class PhysicalOrientation( + val physicalOrientation: io.github.sds100.keymapper.common.utils.PhysicalOrientation, + ) : ConstraintData() { + override val id: ConstraintId = when (physicalOrientation) { + io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT -> ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT + io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE -> ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE + io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT_INVERTED -> ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED + io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE_INVERTED -> ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED } } @@ -316,6 +329,15 @@ object ConstraintEntityMapper { ConstraintEntity.ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait ConstraintEntity.ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape + ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT -> + ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT) + ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE -> + ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE) + ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED -> + ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT_INVERTED) + ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED -> + ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED) + ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn @@ -499,6 +521,25 @@ object ConstraintEntityMapper { ConstraintEntity.ORIENTATION_PORTRAIT, ) + is ConstraintData.PhysicalOrientation -> when (constraint.data.physicalOrientation) { + PhysicalOrientation.PORTRAIT -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT, + ) + PhysicalOrientation.LANDSCAPE -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE, + ) + PhysicalOrientation.PORTRAIT_INVERTED -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, + ) + PhysicalOrientation.LANDSCAPE_INVERTED -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + ) + } + 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 e8b3f784e9..4bbbcf1721 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 @@ -7,6 +7,7 @@ enum class ConstraintDependency { CONNECTED_BT_DEVICES, SCREEN_STATE, DISPLAY_ORIENTATION, + PHYSICAL_ORIENTATION, FLASHLIGHT_STATE, WIFI_SSID, WIFI_STATE, 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 b48b61b8c2..06463c6fd7 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 @@ -19,12 +19,17 @@ enum class ConstraintId { SCREEN_ON, SCREEN_OFF, - ORIENTATION_PORTRAIT, - ORIENTATION_LANDSCAPE, - ORIENTATION_0, - ORIENTATION_90, - ORIENTATION_180, - ORIENTATION_270, + DISPLAY_ORIENTATION_PORTRAIT, + DISPLAY_ORIENTATION_LANDSCAPE, + DISPLAY_ORIENTATION_0, + DISPLAY_ORIENTATION_90, + DISPLAY_ORIENTATION_180, + DISPLAY_ORIENTATION_270, + + PHYSICAL_ORIENTATION_PORTRAIT, + PHYSICAL_ORIENTATION_LANDSCAPE, + PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, + PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, 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 629a610c3a..00ac3781ee 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 @@ -4,6 +4,7 @@ import android.media.AudioManager 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.firstBlocking import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraAdapter @@ -43,6 +44,9 @@ class LazyConstraintSnapshot( devicesAdapter.connectedBluetoothDevices.value } private val orientation: Orientation by lazy { displayAdapter.cachedOrientation } + private val physicalOrientation: PhysicalOrientation by lazy { + displayAdapter.cachedPhysicalOrientation + } private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() } private val appsPlayingMedia: List by lazy { mediaAdapter.getActiveMediaSessionPackages() @@ -117,6 +121,9 @@ class LazyConstraintSnapshot( orientation == Orientation.ORIENTATION_0 || orientation == Orientation.ORIENTATION_180 + is ConstraintData.PhysicalOrientation -> + physicalOrientation == constraint.data.physicalOrientation + is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens) 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 62246d6817..94b02aebc9 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 @@ -6,6 +6,7 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.TimeUtils import io.github.sds100.keymapper.common.utils.handle import io.github.sds100.keymapper.common.utils.valueIfFailure @@ -82,6 +83,21 @@ class ConstraintUiHelper( is ConstraintData.OrientationPortrait -> getString(R.string.constraint_choose_orientation_portrait) + is ConstraintData.PhysicalOrientation -> { + val resId = when (constraint.data.physicalOrientation) { + PhysicalOrientation.PORTRAIT -> + R.string.constraint_choose_physical_orientation_portrait + PhysicalOrientation.LANDSCAPE -> + R.string.constraint_choose_physical_orientation_landscape + PhysicalOrientation.PORTRAIT_INVERTED -> + R.string.constraint_choose_physical_orientation_portrait_inverted + PhysicalOrientation.LANDSCAPE_INVERTED -> + R.string.constraint_choose_physical_orientation_landscape_inverted + } + + getString(resId) + } + is ConstraintData.ScreenOff -> getString(R.string.constraint_screen_off_description) 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 679a1229e1..29377c3533 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 @@ -64,12 +64,16 @@ object ConstraintUtils { ConstraintId.SCREEN_ON, ConstraintId.SCREEN_OFF, - ConstraintId.ORIENTATION_PORTRAIT, - ConstraintId.ORIENTATION_LANDSCAPE, - ConstraintId.ORIENTATION_0, - ConstraintId.ORIENTATION_90, - ConstraintId.ORIENTATION_180, - ConstraintId.ORIENTATION_270, + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT, + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE, + ConstraintId.DISPLAY_ORIENTATION_0, + ConstraintId.DISPLAY_ORIENTATION_90, + ConstraintId.DISPLAY_ORIENTATION_180, + ConstraintId.DISPLAY_ORIENTATION_270, + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT, + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE, + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, -> ConstraintCategory.DISPLAY ConstraintId.FLASHLIGHT_ON, @@ -127,21 +131,29 @@ object ConstraintUtils { Icons.Outlined.BluetoothDisabled, ) - ConstraintId.ORIENTATION_0, - ConstraintId.ORIENTATION_180, + ConstraintId.DISPLAY_ORIENTATION_0, + ConstraintId.DISPLAY_ORIENTATION_180, -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) - ConstraintId.ORIENTATION_90, - ConstraintId.ORIENTATION_270, + ConstraintId.DISPLAY_ORIENTATION_90, + ConstraintId.DISPLAY_ORIENTATION_270, -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) - ConstraintId.ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector( + ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentLandscape, ) - ConstraintId.ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector( + ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ComposeIconInfo.Vector( Icons.Outlined.StayCurrentPortrait, ) + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT, + ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED, + -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE, + ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED, + -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) + ConstraintId.SCREEN_OFF -> ComposeIconInfo.Vector(Icons.Outlined.MobileOff) ConstraintId.SCREEN_ON -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) @@ -194,12 +206,20 @@ 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.ORIENTATION_PORTRAIT -> R.string.constraint_choose_orientation_portrait - ConstraintId.ORIENTATION_LANDSCAPE -> R.string.constraint_choose_orientation_landscape - ConstraintId.ORIENTATION_0 -> R.string.constraint_choose_orientation_0 - ConstraintId.ORIENTATION_90 -> R.string.constraint_choose_orientation_90 - ConstraintId.ORIENTATION_180 -> R.string.constraint_choose_orientation_180 - ConstraintId.ORIENTATION_270 -> R.string.constraint_choose_orientation_270 + 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 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 87e38e6af3..8bf75d6f24 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 @@ -71,6 +71,8 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( 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), 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 bb53bd5f66..a48843773b 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 @@ -140,6 +140,9 @@ class KeyMapConstraintsComparator( ConstraintData.HingeOpen -> Success("") ConstraintData.KeyboardNotShowing -> Success("") ConstraintData.KeyboardShowing -> Success("") + is ConstraintData.PhysicalOrientation -> Success( + constraint.data.physicalOrientation.toString(), + ) } } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3fd683d2f9..5e243dcdbd 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -256,6 +256,12 @@ Landscape (270°) Portrait (any) Landscape (any) + Screen orientation + Physical orientation + Physical: Portrait + Physical: Landscape + Physical: Portrait (upside down) + Physical: Landscape (inverted) 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 063557172f..be77315302 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 @@ -4,6 +4,7 @@ import io.github.sds100.keymapper.base.constraints.Constraint 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.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.foldable.HingeState @@ -17,6 +18,7 @@ class TestConstraintSnapshot( val appInForeground: String? = null, val connectedBluetoothDevices: Set = emptySet(), val orientation: Orientation = Orientation.ORIENTATION_0, + val physicalOrientation: PhysicalOrientation = PhysicalOrientation.PORTRAIT, val isScreenOn: Boolean = false, val appsPlayingMedia: List = emptyList(), val isWifiEnabled: Boolean = false, @@ -62,6 +64,9 @@ class TestConstraintSnapshot( orientation == Orientation.ORIENTATION_0 || orientation == Orientation.ORIENTATION_180 + is ConstraintData.PhysicalOrientation -> + physicalOrientation == data.physicalOrientation + is ConstraintData.ScreenOff -> !isScreenOn is ConstraintData.ScreenOn -> isScreenOn is ConstraintData.FlashlightOff -> when (data.lens) { diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt new file mode 100644 index 0000000000..a89aa9a2d2 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/PhysicalOrientation.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.common.utils + +/** + * Represents the physical orientation of the device based on the device's + * orientation sensor (accelerometer), independent of the screen rotation setting. + */ +enum class PhysicalOrientation { + PORTRAIT, + LANDSCAPE, + PORTRAIT_INVERTED, + LANDSCAPE_INVERTED, +} 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 24d5954c60..a7733dd66a 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 @@ -58,6 +58,11 @@ data class ConstraintEntity( const val ORIENTATION_PORTRAIT = "constraint_orientation_portrait" const val ORIENTATION_LANDSCAPE = "constraint_orientation_landscape" + const val PHYSICAL_ORIENTATION_PORTRAIT = "constraint_physical_orientation_portrait" + const val PHYSICAL_ORIENTATION_LANDSCAPE = "constraint_physical_orientation_landscape" + const val PHYSICAL_ORIENTATION_PORTRAIT_INVERTED = "constraint_physical_orientation_portrait_inverted" + const val PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED = "constraint_physical_orientation_landscape_inverted" + const val FLASHLIGHT_ON = "flashlight_on" const val FLASHLIGHT_OFF = "flashlight_off" 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 d234b680aa..560e60e42d 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 @@ -7,6 +7,7 @@ import android.content.IntentFilter import android.hardware.display.DisplayManager import android.provider.Settings import android.view.Display +import android.view.OrientationEventListener import android.view.Surface import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -14,6 +15,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.PhysicalOrientation import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.Success @@ -28,6 +30,14 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +/** + * Android implementation of DisplayAdapter. + * + * This is a Singleton that lives for the lifetime of the application. + * Listeners (DisplayManager.DisplayListener, BroadcastReceiver, OrientationEventListener) + * are intentionally not unregistered because the adapter needs to continuously track + * display state changes throughout the app's lifecycle. + */ @Singleton class AndroidDisplayAdapter @Inject constructor( @ApplicationContext private val context: Context, @@ -39,6 +49,12 @@ class AndroidDisplayAdapter @Inject constructor( * How much to change the brightness by. */ private const val BRIGHTNESS_CHANGE_STEP = 20 + + /** + * Tolerance in degrees for orientation detection. + * This helps avoid rapid switching at orientation boundaries. + */ + private const val ORIENTATION_TOLERANCE = 45 } private val ctx = context.applicationContext @@ -77,12 +93,28 @@ class AndroidDisplayAdapter @Inject constructor( override val cachedOrientation: Orientation get() = _orientation.value + private val _physicalOrientation = MutableStateFlow(PhysicalOrientation.PORTRAIT) + override val physicalOrientation: Flow = _physicalOrientation + override val cachedPhysicalOrientation: PhysicalOrientation + get() = _physicalOrientation.value + override val size: SizeKM get() = ctx.getRealDisplaySize() override val isAmbientDisplayEnabled: MutableStateFlow = MutableStateFlow(isAodEnabled()) + private val orientationEventListener = object : OrientationEventListener(ctx) { + override fun onOrientationChanged(orientationDegrees: Int) { + if (orientationDegrees == ORIENTATION_UNKNOWN) { + return + } + + val newPhysicalOrientation = degreesToPhysicalOrientation(orientationDegrees) + _physicalOrientation.update { newPhysicalOrientation } + } + } + init { displayManager.registerDisplayListener( object : DisplayManager.DisplayListener { @@ -115,6 +147,11 @@ class AndroidDisplayAdapter @Inject constructor( filter, ContextCompat.RECEIVER_NOT_EXPORTED, ) + + // Enable physical orientation detection + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable() + } } override fun isAutoRotateEnabled(): Boolean = @@ -253,4 +290,36 @@ class AndroidDisplayAdapter @Inject constructor( private fun isAodEnabled(): Boolean { return SettingsUtils.getSecureSetting(ctx, "doze_always_on") == 1 } + + /** + * Converts sensor orientation degrees to PhysicalOrientation. + * + * The orientation degrees from OrientationEventListener represent how much + * the device is rotated from its natural orientation: + * - 0°: Device is upright (portrait for most phones) + * - 90°: Device is rotated 90° counter-clockwise (landscape, home button on right) + * - 180°: Device is upside down (portrait inverted) + * - 270°: Device is rotated 90° clockwise (landscape, home button on left) + * + * Using a tolerance helps avoid rapid orientation changes at boundaries. + */ + private fun degreesToPhysicalOrientation(degrees: Int): PhysicalOrientation { + // OrientationEventListener returns 0-359 degrees. + // Handle wraparound at 0/360 boundary for portrait detection. + // Degrees outside the defined tolerance zones (e.g., 45-89° between portrait and landscape) + // are intentionally kept as the current orientation to provide hysteresis and prevent + // rapid orientation switching when the device is tilted at boundary angles. + return when { + 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 + } + } } 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 f8792d6fd4..377e148044 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 @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.system.display import io.github.sds100.keymapper.common.utils.KMResult 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 @@ -9,6 +10,8 @@ interface DisplayAdapter { val isScreenOn: Flow val orientation: Flow val cachedOrientation: Orientation + val physicalOrientation: Flow + val cachedPhysicalOrientation: PhysicalOrientation val size: SizeKM val isAmbientDisplayEnabled: Flow