Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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))
Expand Down Expand Up @@ -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<SimpleListItemGroup> = 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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum class ConstraintDependency {
CONNECTED_BT_DEVICES,
SCREEN_STATE,
DISPLAY_ORIENTATION,
PHYSICAL_ORIENTATION,
FLASHLIGHT_STATE,
WIFI_SSID,
WIFI_STATE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> by lazy {
mediaAdapter.getActiveMediaSessionPackages()
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading