Skip to content

Commit 9b80a8c

Browse files
authored
Merge pull request #1911 from keymapperorg/copilot/add-phone-orientation-constraint
Add physical orientation constraint using OrientationEventListener
2 parents 6e3c171 + 119b776 commit 9b80a8c

File tree

16 files changed

+384
-41
lines changed

16 files changed

+384
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- #1915 ask user to remove "adb shell" from Shell command.
88
- #1904 inform the user how to enable the accessibility service with PRO mode or ADB.
9+
- #1911 constraint for physical device orientation that ignores auto rotate setting.
910

1011
## Bug fixes
1112

base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt

Lines changed: 158 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.sds100.keymapper.base.constraints
22

3+
import androidx.compose.material.icons.Icons
4+
import androidx.compose.material.icons.outlined.StayCurrentPortrait
35
import androidx.compose.runtime.getValue
46
import androidx.compose.runtime.mutableStateOf
57
import androidx.compose.runtime.setValue
@@ -15,10 +17,12 @@ import io.github.sds100.keymapper.base.utils.navigation.navigate
1517
import io.github.sds100.keymapper.base.utils.ui.DialogModel
1618
import io.github.sds100.keymapper.base.utils.ui.DialogProvider
1719
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
20+
import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
1821
import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemGroup
1922
import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel
2023
import io.github.sds100.keymapper.base.utils.ui.showDialog
2124
import io.github.sds100.keymapper.common.utils.Orientation
25+
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
2226
import io.github.sds100.keymapper.common.utils.State
2327
import io.github.sds100.keymapper.system.camera.CameraLens
2428
import javax.inject.Inject
@@ -46,6 +50,10 @@ class ChooseConstraintViewModel @Inject constructor(
4650
NavigationProvider by navigationProvider {
4751

4852
companion object {
53+
// Synthetic IDs for consolidated orientation list items (not actual ConstraintIds)
54+
private const val DISPLAY_ORIENTATION_LIST_ITEM_ID = "display_orientation"
55+
private const val PHYSICAL_ORIENTATION_LIST_ITEM_ID = "physical_orientation"
56+
4957
private val CATEGORY_ORDER = arrayOf(
5058
ConstraintCategory.APPS,
5159
ConstraintCategory.MEDIA,
@@ -111,6 +119,18 @@ class ChooseConstraintViewModel @Inject constructor(
111119

112120
fun onListItemClick(id: String) {
113121
viewModelScope.launch {
122+
// Handle synthetic list item IDs for consolidated orientation constraints
123+
when (id) {
124+
DISPLAY_ORIENTATION_LIST_ITEM_ID -> {
125+
onSelectDisplayOrientationConstraint()
126+
return@launch
127+
}
128+
PHYSICAL_ORIENTATION_LIST_ITEM_ID -> {
129+
onSelectPhysicalOrientationConstraint()
130+
return@launch
131+
}
132+
}
133+
114134
when (val constraintType = ConstraintId.valueOf(id)) {
115135
ConstraintId.APP_IN_FOREGROUND,
116136
ConstraintId.APP_NOT_IN_FOREGROUND,
@@ -131,32 +151,60 @@ class ChooseConstraintViewModel @Inject constructor(
131151

132152
ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff)
133153

134-
ConstraintId.ORIENTATION_PORTRAIT ->
154+
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT ->
135155
returnResult.emit(ConstraintData.OrientationPortrait)
136156

137-
ConstraintId.ORIENTATION_LANDSCAPE ->
157+
ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE ->
138158
returnResult.emit(ConstraintData.OrientationLandscape)
139159

140-
ConstraintId.ORIENTATION_0 ->
160+
ConstraintId.DISPLAY_ORIENTATION_0 ->
141161
returnResult.emit(
142162
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0),
143163
)
144164

145-
ConstraintId.ORIENTATION_90 ->
165+
ConstraintId.DISPLAY_ORIENTATION_90 ->
146166
returnResult.emit(
147167
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90),
148168
)
149169

150-
ConstraintId.ORIENTATION_180 ->
170+
ConstraintId.DISPLAY_ORIENTATION_180 ->
151171
returnResult.emit(
152172
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180),
153173
)
154174

155-
ConstraintId.ORIENTATION_270 ->
175+
ConstraintId.DISPLAY_ORIENTATION_270 ->
156176
returnResult.emit(
157177
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270),
158178
)
159179

180+
ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT ->
181+
returnResult.emit(
182+
ConstraintData.PhysicalOrientation(
183+
physicalOrientation = PhysicalOrientation.PORTRAIT,
184+
),
185+
)
186+
187+
ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE ->
188+
returnResult.emit(
189+
ConstraintData.PhysicalOrientation(
190+
physicalOrientation = PhysicalOrientation.LANDSCAPE,
191+
),
192+
)
193+
194+
ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED ->
195+
returnResult.emit(
196+
ConstraintData.PhysicalOrientation(
197+
physicalOrientation = PhysicalOrientation.PORTRAIT_INVERTED,
198+
),
199+
)
200+
201+
ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
202+
returnResult.emit(
203+
ConstraintData.PhysicalOrientation(
204+
physicalOrientation = PhysicalOrientation.LANDSCAPE_INVERTED,
205+
),
206+
)
207+
160208
ConstraintId.FLASHLIGHT_ON -> {
161209
val lens = chooseFlashlightLens() ?: return@launch
162210
returnResult.emit(ConstraintData.FlashlightOn(lens = lens))
@@ -251,18 +299,117 @@ class ChooseConstraintViewModel @Inject constructor(
251299
return cameraLens
252300
}
253301

302+
private suspend fun onSelectDisplayOrientationConstraint() {
303+
val items = listOf(
304+
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT to
305+
getString(R.string.constraint_choose_orientation_portrait),
306+
ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE to
307+
getString(R.string.constraint_choose_orientation_landscape),
308+
ConstraintId.DISPLAY_ORIENTATION_0 to
309+
getString(R.string.constraint_choose_orientation_0),
310+
ConstraintId.DISPLAY_ORIENTATION_90 to
311+
getString(R.string.constraint_choose_orientation_90),
312+
ConstraintId.DISPLAY_ORIENTATION_180 to
313+
getString(R.string.constraint_choose_orientation_180),
314+
ConstraintId.DISPLAY_ORIENTATION_270 to
315+
getString(R.string.constraint_choose_orientation_270),
316+
)
317+
318+
val dialog = DialogModel.SingleChoice(items)
319+
val selectedOrientation = showDialog("choose_display_orientation", dialog) ?: return
320+
321+
val constraintData = when (selectedOrientation) {
322+
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait
323+
ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape
324+
ConstraintId.DISPLAY_ORIENTATION_0 ->
325+
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0)
326+
ConstraintId.DISPLAY_ORIENTATION_90 ->
327+
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90)
328+
ConstraintId.DISPLAY_ORIENTATION_180 ->
329+
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180)
330+
ConstraintId.DISPLAY_ORIENTATION_270 ->
331+
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270)
332+
else -> return
333+
}
334+
335+
returnResult.emit(constraintData)
336+
}
337+
338+
private suspend fun onSelectPhysicalOrientationConstraint() {
339+
val items = listOf(
340+
PhysicalOrientation.PORTRAIT to
341+
getString(R.string.constraint_choose_physical_orientation_portrait),
342+
PhysicalOrientation.LANDSCAPE to
343+
getString(R.string.constraint_choose_physical_orientation_landscape),
344+
PhysicalOrientation.PORTRAIT_INVERTED to
345+
getString(R.string.constraint_choose_physical_orientation_portrait_inverted),
346+
PhysicalOrientation.LANDSCAPE_INVERTED to
347+
getString(R.string.constraint_choose_physical_orientation_landscape_inverted),
348+
)
349+
350+
val dialog = DialogModel.SingleChoice(items)
351+
val selectedOrientation = showDialog("choose_physical_orientation", dialog) ?: return
352+
353+
returnResult.emit(
354+
ConstraintData.PhysicalOrientation(physicalOrientation = selectedOrientation),
355+
)
356+
}
357+
254358
private fun buildListGroups(): List<SimpleListItemGroup> = buildList {
255-
val listItems = buildListItems(ConstraintId.entries)
359+
// Filter out individual orientation constraints - show only the consolidated ones
360+
val filteredConstraints = ConstraintId.entries.filter { constraintId ->
361+
constraintId !in listOf(
362+
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT,
363+
ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE,
364+
ConstraintId.DISPLAY_ORIENTATION_0,
365+
ConstraintId.DISPLAY_ORIENTATION_90,
366+
ConstraintId.DISPLAY_ORIENTATION_180,
367+
ConstraintId.DISPLAY_ORIENTATION_270,
368+
ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT,
369+
ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE,
370+
ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
371+
ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
372+
)
373+
}
374+
375+
val listItems = buildListItems(filteredConstraints)
376+
377+
// Add synthetic orientation list items
378+
val displayOrientationItem = SimpleListItemModel(
379+
id = DISPLAY_ORIENTATION_LIST_ITEM_ID,
380+
title = getString(R.string.constraint_choose_screen_orientation),
381+
icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait),
382+
isEnabled = true,
383+
)
384+
385+
val physicalOrientationItem = SimpleListItemModel(
386+
id = PHYSICAL_ORIENTATION_LIST_ITEM_ID,
387+
title = getString(R.string.constraint_choose_physical_orientation),
388+
icon = ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait),
389+
isEnabled = true,
390+
)
256391

257392
for (category in CATEGORY_ORDER) {
258393
val header = getString(ConstraintUtils.getCategoryLabel(category))
259394

395+
val categoryItems = listItems.filter { item ->
396+
item.isEnabled &&
397+
try {
398+
ConstraintUtils.getCategory(ConstraintId.valueOf(item.id)) == category
399+
} catch (e: IllegalArgumentException) {
400+
false
401+
}
402+
}.toMutableList()
403+
404+
// Add synthetic orientation items to DISPLAY category
405+
if (category == ConstraintCategory.DISPLAY) {
406+
categoryItems.add(displayOrientationItem)
407+
categoryItems.add(physicalOrientationItem)
408+
}
409+
260410
val group = SimpleListItemGroup(
261411
header,
262-
items = listItems.filter {
263-
it.isEnabled &&
264-
ConstraintUtils.getCategory(ConstraintId.valueOf(it.id)) == category
265-
},
412+
items = categoryItems,
266413
)
267414

268415
if (group.items.isNotEmpty()) {

base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.sds100.keymapper.base.constraints
22

33
import io.github.sds100.keymapper.common.utils.Orientation
4+
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
45
import io.github.sds100.keymapper.common.utils.getKey
56
import io.github.sds100.keymapper.common.utils.valueOrNull
67
import io.github.sds100.keymapper.data.entities.ConstraintEntity
@@ -69,21 +70,33 @@ sealed class ConstraintData {
6970

7071
@Serializable
7172
data object OrientationPortrait : ConstraintData() {
72-
override val id: ConstraintId = ConstraintId.ORIENTATION_PORTRAIT
73+
override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_PORTRAIT
7374
}
7475

7576
@Serializable
7677
data object OrientationLandscape : ConstraintData() {
77-
override val id: ConstraintId = ConstraintId.ORIENTATION_LANDSCAPE
78+
override val id: ConstraintId = ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE
7879
}
7980

8081
@Serializable
8182
data class OrientationCustom(val orientation: Orientation) : ConstraintData() {
8283
override val id: ConstraintId = when (orientation) {
83-
Orientation.ORIENTATION_0 -> ConstraintId.ORIENTATION_0
84-
Orientation.ORIENTATION_90 -> ConstraintId.ORIENTATION_90
85-
Orientation.ORIENTATION_180 -> ConstraintId.ORIENTATION_180
86-
Orientation.ORIENTATION_270 -> ConstraintId.ORIENTATION_270
84+
Orientation.ORIENTATION_0 -> ConstraintId.DISPLAY_ORIENTATION_0
85+
Orientation.ORIENTATION_90 -> ConstraintId.DISPLAY_ORIENTATION_90
86+
Orientation.ORIENTATION_180 -> ConstraintId.DISPLAY_ORIENTATION_180
87+
Orientation.ORIENTATION_270 -> ConstraintId.DISPLAY_ORIENTATION_270
88+
}
89+
}
90+
91+
@Serializable
92+
data class PhysicalOrientation(
93+
val physicalOrientation: io.github.sds100.keymapper.common.utils.PhysicalOrientation,
94+
) : ConstraintData() {
95+
override val id: ConstraintId = when (physicalOrientation) {
96+
io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT -> ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT
97+
io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE -> ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE
98+
io.github.sds100.keymapper.common.utils.PhysicalOrientation.PORTRAIT_INVERTED -> ConstraintId.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED
99+
io.github.sds100.keymapper.common.utils.PhysicalOrientation.LANDSCAPE_INVERTED -> ConstraintId.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED
87100
}
88101
}
89102

@@ -316,6 +329,15 @@ object ConstraintEntityMapper {
316329
ConstraintEntity.ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait
317330
ConstraintEntity.ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape
318331

332+
ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT ->
333+
ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT)
334+
ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE ->
335+
ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE)
336+
ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED ->
337+
ConstraintData.PhysicalOrientation(PhysicalOrientation.PORTRAIT_INVERTED)
338+
ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
339+
ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED)
340+
319341
ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff
320342
ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn
321343

@@ -499,6 +521,25 @@ object ConstraintEntityMapper {
499521
ConstraintEntity.ORIENTATION_PORTRAIT,
500522
)
501523

524+
is ConstraintData.PhysicalOrientation -> when (constraint.data.physicalOrientation) {
525+
PhysicalOrientation.PORTRAIT -> ConstraintEntity(
526+
uid = constraint.uid,
527+
ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT,
528+
)
529+
PhysicalOrientation.LANDSCAPE -> ConstraintEntity(
530+
uid = constraint.uid,
531+
ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE,
532+
)
533+
PhysicalOrientation.PORTRAIT_INVERTED -> ConstraintEntity(
534+
uid = constraint.uid,
535+
ConstraintEntity.PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
536+
)
537+
PhysicalOrientation.LANDSCAPE_INVERTED -> ConstraintEntity(
538+
uid = constraint.uid,
539+
ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
540+
)
541+
}
542+
502543
is ConstraintData.ScreenOff -> ConstraintEntity(
503544
uid = constraint.uid,
504545
ConstraintEntity.SCREEN_OFF,

base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ enum class ConstraintDependency {
77
CONNECTED_BT_DEVICES,
88
SCREEN_STATE,
99
DISPLAY_ORIENTATION,
10+
PHYSICAL_ORIENTATION,
1011
FLASHLIGHT_STATE,
1112
WIFI_SSID,
1213
WIFI_STATE,

base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ enum class ConstraintId {
1919
SCREEN_ON,
2020
SCREEN_OFF,
2121

22-
ORIENTATION_PORTRAIT,
23-
ORIENTATION_LANDSCAPE,
24-
ORIENTATION_0,
25-
ORIENTATION_90,
26-
ORIENTATION_180,
27-
ORIENTATION_270,
22+
DISPLAY_ORIENTATION_PORTRAIT,
23+
DISPLAY_ORIENTATION_LANDSCAPE,
24+
DISPLAY_ORIENTATION_0,
25+
DISPLAY_ORIENTATION_90,
26+
DISPLAY_ORIENTATION_180,
27+
DISPLAY_ORIENTATION_270,
28+
29+
PHYSICAL_ORIENTATION_PORTRAIT,
30+
PHYSICAL_ORIENTATION_LANDSCAPE,
31+
PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
32+
PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,
2833

2934
FLASHLIGHT_ON,
3035
FLASHLIGHT_OFF,

base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.media.AudioManager
44
import android.os.Build
55
import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService
66
import io.github.sds100.keymapper.common.utils.Orientation
7+
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
78
import io.github.sds100.keymapper.common.utils.firstBlocking
89
import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo
910
import io.github.sds100.keymapper.system.camera.CameraAdapter
@@ -43,6 +44,9 @@ class LazyConstraintSnapshot(
4344
devicesAdapter.connectedBluetoothDevices.value
4445
}
4546
private val orientation: Orientation by lazy { displayAdapter.cachedOrientation }
47+
private val physicalOrientation: PhysicalOrientation by lazy {
48+
displayAdapter.cachedPhysicalOrientation
49+
}
4650
private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() }
4751
private val appsPlayingMedia: List<String> by lazy {
4852
mediaAdapter.getActiveMediaSessionPackages()
@@ -117,6 +121,9 @@ class LazyConstraintSnapshot(
117121
orientation == Orientation.ORIENTATION_0 ||
118122
orientation == Orientation.ORIENTATION_180
119123

124+
is ConstraintData.PhysicalOrientation ->
125+
physicalOrientation == constraint.data.physicalOrientation
126+
120127
is ConstraintData.ScreenOff -> !isScreenOn
121128
is ConstraintData.ScreenOn -> isScreenOn
122129
is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens)

0 commit comments

Comments
 (0)