diff --git a/CHANGELOG.md b/CHANGELOG.md index b07fe699aa..2a586617bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [4.6.1](https://github.com/sds100/KeyMapper/releases/tag/v4.6.1) + +#### 10 October 2026 + +## Changed + +- #2260 the "Do not remap" trigger key option is now called "Passthrough key events". + +## Fixed + +- Remove button is visible on key map group constraints with long text. +- #2290 Regression in 4.6. Place floating buttons correctly and tap actions when the display is rotated or resolution is changed. +- #2287 Key maps that launch different apps open the same app when the device is locked. +- #2289 Expert Mode setup and auto start no longer get stuck on Android 17 devices that hide whether developer options are enabled. +- Do not crash if WRITE_SECURE_SETTINGS permission not granted and disabling wireless debugging after starting system bridge. +- #2294 On Android 17 QPR1 enabling USB debugging while Wireless Debugging is enabled kills the Expert Mode system bridge on startup. + ## [4.6.0](https://github.com/sds100/KeyMapper/releases/tag/v4.6.0) #### 4 October 2026 diff --git a/app/version.properties b/app/version.properties index 14457a9750..86b9f20735 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.6.0 -VERSION_CODE=278 +VERSION_NAME=4.6.1 +VERSION_CODE=286 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index c8f671262e..64a5fbacc3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -379,7 +379,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.TapScreen -> { - val displaySize = getActiveDisplay()!!.activeSize + val displaySize = getActiveDisplay()!!.logicalSize val point = scaleCoordinate(action.x, action.y, action.screenResolution, displaySize) @@ -387,7 +387,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.SwipeScreen -> { - val displaySize = getActiveDisplay()!!.activeSize + val displaySize = getActiveDisplay()!!.logicalSize val start = scaleCoordinate( action.xStart, action.yStart, @@ -409,7 +409,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.PinchScreen -> { - val displaySize = getActiveDisplay()!!.activeSize + val displaySize = getActiveDisplay()!!.logicalSize val point = scaleCoordinate(action.x, action.y, action.screenResolution, displaySize) val distance = diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ScreenshotPickerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ScreenshotPickerDelegate.kt index 960c7366d7..8efe9cc5ce 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ScreenshotPickerDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ScreenshotPickerDelegate.kt @@ -73,7 +73,7 @@ class ScreenshotPickerDelegate( val activeDisplay = displayAdapter.getDisplay(displayAdapter.activityDisplayId ?: Display.DEFAULT_DISPLAY)!! - return screenshotResolution.value ?: loadedResolution.value ?: activeDisplay.activeSize + return screenshotResolution.value ?: loadedResolution.value ?: activeDisplay.logicalSize } fun recycle() { 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 4ca3e84d0f..252b9c4e19 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 @@ -60,7 +60,7 @@ class LazyConstraintSnapshot( } private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() } private val displaySize: SizeKM by lazy { - getActiveDisplay()!!.activeSize + getActiveDisplay()!!.physicalSize } private val appsPlayingMedia: List by lazy { 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 c026b870a6..b143fd099b 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 @@ -68,7 +68,7 @@ class CreateConstraintUseCaseImpl @Inject constructor( override fun getCurrentResolution(): SizeKM { return displayAdapter.getDisplay( displayAdapter.activityDisplayId ?: Display.DEFAULT_DISPLAY, - )!!.activeSize + )!!.physicalSize } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt index 5002af3262..87ea131d57 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -73,6 +74,7 @@ fun ExpertModeSetupScreen(viewModel: ExpertModeSetupViewModel) { onWatchTutorialClick = { }, onBackClick = viewModel::onBackClick, onSamsungAutoBlockerWarningClick = viewModel::onSamsungAutoBlockerWarningClick, + onSkipDeveloperOptionsClick = viewModel::onSkipDeveloperOptionsClick, ) } @@ -85,6 +87,7 @@ fun ExpertModeSetupScreen( onAssistantClick: () -> Unit = {}, onWatchTutorialClick: () -> Unit = {}, onSamsungAutoBlockerWarningClick: () -> Unit = {}, + onSkipDeveloperOptionsClick: () -> Unit = {}, ) { Scaffold( topBar = { @@ -110,6 +113,7 @@ fun ExpertModeSetupScreen( onWatchTutorialClick, onStepButtonClick, onSamsungAutoBlockerWarningClick, + onSkipDeveloperOptionsClick, ) } } @@ -122,6 +126,7 @@ fun ExpertModeSetupScreenContent( onWatchTutorialClick: () -> Unit, onStepButtonClick: () -> Unit, onSamsungAutoBlockerWarningClick: () -> Unit = {}, + onSkipDeveloperOptionsClick: () -> Unit = {}, ) { when (state) { State.Loading -> { @@ -211,10 +216,6 @@ fun ExpertModeSetupScreenContent( val isStarting = stepData is ExpertModeSetupState.StartService && stepData.isStarting - val showSamsungAutoBlockerWarning = - stepData is ExpertModeSetupState.WirelessDebugging && - stepData.showSamsungAutoBlockerWarning - StepContent( modifier = Modifier .fillMaxWidth() @@ -225,14 +226,26 @@ fun ExpertModeSetupScreenContent( onButtonClick = onStepButtonClick, iconTint = iconTint, isLoading = isStarting, - warningContent = if (showSamsungAutoBlockerWarning) { - { - SamsungAutoBlockerWarningCard( - onClick = onSamsungAutoBlockerWarningClick, - ) + warningContent = when { + stepData is ExpertModeSetupState.WirelessDebugging && + stepData.showSamsungAutoBlockerWarning -> { + { + SamsungAutoBlockerWarningCard( + onClick = onSamsungAutoBlockerWarningClick, + ) + } + } + + stepData is ExpertModeSetupState.DeveloperOptions && + stepData.showRedactedWarning -> { + { + DeveloperOptionsRedactedWarningCard( + onClick = onSkipDeveloperOptionsClick, + ) + } } - } else { - null + + else -> null }, ) } @@ -331,6 +344,26 @@ private fun SamsungAutoBlockerWarningCard(modifier: Modifier = Modifier, onClick ) } +@Composable +private fun DeveloperOptionsRedactedWarningCard( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + SetupWarningCard( + modifier = modifier, + title = stringResource( + R.string.expert_mode_setup_wizard_developer_options_redacted_warning_title, + ), + description = stringResource( + R.string.expert_mode_setup_wizard_developer_options_redacted_warning_description, + ), + buttonText = stringResource( + R.string.expert_mode_setup_wizard_developer_options_redacted_warning_button, + ), + onButtonClick = onClick, + ) +} + @Composable private fun SetupWarningCard( modifier: Modifier = Modifier, @@ -343,6 +376,7 @@ private fun SetupWarningCard( Column( modifier = Modifier .fillMaxWidth() + .widthIn(max = 400.dp) .padding(horizontal = 16.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -603,6 +637,7 @@ private fun ExpertModeSetupScreenDeveloperOptionsPreview() { stepNumber = 5, stepCount = 10, isSetupAssistantChecked = false, + showRedactedWarning = true, ), ), ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt index ebf8eb122d..1de5ac96ec 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/ExpertModeSetupState.kt @@ -40,6 +40,7 @@ sealed class ExpertModeSetupState { override val stepNumber: Int, override val stepCount: Int, override val isSetupAssistantChecked: Boolean, + val showRedactedWarning: Boolean, ) : ExpertModeSetupState() { override val isSetupAssistantButtonEnabled: Boolean = true } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt index cf06ba204c..2ae262d9b5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupAssistantController.kt @@ -304,8 +304,8 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( ) { val notification = NotificationModel( // Use the same notification id for all so they overwrite each other. - id = NotificationController.Companion.ID_SETUP_ASSISTANT, - channel = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, + id = NotificationController.ID_SETUP_ASSISTANT, + channel = NotificationController.CHANNEL_SETUP_ASSISTANT, title = title, text = text, icon = R.drawable.offline_bolt_24px, @@ -322,7 +322,7 @@ class SystemBridgeSetupAssistantController @AssistedInject constructor( } private fun dismissNotification() { - manageNotifications.dismiss(NotificationController.Companion.ID_SETUP_ASSISTANT) + manageNotifications.dismiss(NotificationController.ID_SETUP_ASSISTANT) } private fun findPairingCodeText(rootNode: AccessibilityNodeInfo): String? { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt index 9fe4e6111f..90dc1e5f15 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupDelegate.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.expertmode +import android.os.Build import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep @@ -57,6 +58,10 @@ abstract class SystemBridgeSetupDelegateImpl( useCase.openSamsungAutoBlockerSettings() } + override fun onSkipDeveloperOptionsClick() { + useCase.skipDeveloperOptionsStep() + } + private fun buildState( step: SystemBridgeSetupStep, isSetupAssistantUserEnabled: Boolean, @@ -99,6 +104,9 @@ abstract class SystemBridgeSetupDelegateImpl( stepNumber = stepNumber, stepCount = stepCount, isSetupAssistantChecked = isSetupAssistantUserEnabled, + // See issue #2289. The developer options setting can be redacted on Android 17+. + showRedactedWarning = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN, ) SystemBridgeSetupStep.WIFI_NETWORK -> @@ -148,4 +156,5 @@ interface SystemBridgeSetupDelegate { fun onSetupStepButtonClick() fun onSetupAssistantClick() fun onSamsungAutoBlockerWarningClick() + fun onSkipDeveloperOptionsClick() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt index 8318d3afa9..e71c3e3944 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCase.kt @@ -74,6 +74,17 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( systemBridgeSetupController.openAutoBlockerSettings() } + /** + * See issue #2289. Android 17 can redact the developer options setting so it always + * reads as disabled. Let the user skip the step if they know it is enabled. + * Not persisted. + */ + private val isDeveloperOptionsSkipped = MutableStateFlow(false) + + override fun skipDeveloperOptionsStep() { + isDeveloperOptionsSkipped.value = true + } + override val isWarningUnderstood: Flow = preferences.get(Keys.isExpertModeWarningUnderstood).map { it ?: false } @@ -169,7 +180,10 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( combine( isNotificationPermissionGranted, isLocalNetworkPermissionGranted, - systemBridgeSetupController.isDeveloperOptionsEnabled, + combine( + systemBridgeSetupController.isDeveloperOptionsEnabled, + isDeveloperOptionsSkipped, + ) { isEnabled, isSkipped -> isEnabled || isSkipped }, networkAdapter.isWifiConnected, systemBridgeSetupController.isWirelessDebuggingEnabled, ) { @@ -368,7 +382,10 @@ class SystemBridgeSetupUseCaseImpl @Inject constructor( needsSamsungAutoBlockerStep -> SystemBridgeSetupStep.SAMSUNG_AUTO_BLOCKER - !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS + // Wireless debugging can not be enabled without developer options being enabled. + // Check both because the developer options setting can be redacted. See #2289. + !isDeveloperOptionsEnabled && !isWirelessDebuggingEnabled -> + SystemBridgeSetupStep.DEVELOPER_OPTIONS !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK @@ -425,6 +442,7 @@ interface SystemBridgeSetupUseCase { fun stopSystemBridge() fun enableAccessibilityService() fun enableDeveloperOptions() + fun skipDeveloperOptionsStep() fun launchDeveloperOptions() fun connectWifiNetwork() fun enableWirelessDebugging() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt index c829e9e8e8..dc3a47a18e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/floating/FloatingButtonData.kt @@ -27,14 +27,23 @@ data class FloatingButtonData( */ @Serializable data class Location( + /** + * The location in the orientation that the display was when this + * floating button was created. + */ val x: Int, + /** + * The location in the orientation that the display was when this + * floating button was created. + */ val y: Int, /** * The orientation the screen was in when the user picked the location of the button. */ val orientation: Orientation, /** - * The size of the display this button was placed on. + * The size of the display in the orientation this button was placed in, + * not the natural size. */ val displaySize: SizeKM, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/groups/GroupConstraintRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/groups/GroupConstraintRow.kt index 0304f76cdf..b48f02c710 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/groups/GroupConstraintRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/groups/GroupConstraintRow.kt @@ -241,7 +241,7 @@ private fun PreviewOneItem() { } } -@Preview +@Preview(widthDp = 300) @Composable private fun PreviewMultipleItems() { val ctx = LocalContext.current @@ -267,7 +267,7 @@ private fun PreviewMultipleItems() { ), ComposeChipModel.Error( id = "2", - text = "Key Mapper not found", + text = "Very very very very long app name not found", error = KMError.AppNotFound("io.github.sds100.keymapper"), ), ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/WhatsNewDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/WhatsNewDialog.kt index 90b816fd94..7fae41ac36 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/WhatsNewDialog.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/WhatsNewDialog.kt @@ -6,11 +6,14 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize 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.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -69,11 +72,16 @@ fun WhatsNewDialogContent( val uriHandler = LocalUriHandler.current val changelogUrl = stringResource(R.string.url_changelog) - Column(modifier = modifier.widthIn(max = 400.dp)) { + Column( + modifier = modifier + .width(IntrinsicSize.Max).widthIn(max = 400.dp), + ) { Spacer(Modifier.height(16.dp)) Column( - modifier = modifier.verticalScroll(rememberScrollState()).weight(1f, fill = false), + modifier = modifier + .verticalScroll(rememberScrollState()) + .weight(1f, fill = false), verticalArrangement = Arrangement.spacedBy(8.dp), ) { WhatsNewHeader( @@ -167,7 +175,10 @@ private fun WhatsNewSection( return } - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { OptionsHeaderRow( modifier = Modifier.padding(horizontal = 24.dp), icon = icon, @@ -177,7 +188,9 @@ private fun WhatsNewSection( for (item in items) { if (item.isSpotlight) { Surface( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), color = MaterialTheme.colorScheme.surfaceContainerHighest, border = BorderStroke( width = 1.dp, @@ -226,6 +239,11 @@ private val previewState = WhatsNewState( description = "Switch between your keyboard's languages with a key press", isSpotlight = true, ), + WhatsNewItem( + title = "New action", + description = "Short text", + isSpotlight = true, + ), WhatsNewItem("Sort key maps by enabled/disabled", null), WhatsNewItem("Export/import key maps on Android TV", null), ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt index a93e1516c7..2da3d0dc0b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -219,7 +218,7 @@ private fun Content( .padding(horizontal = 16.dp), title = stringResource(R.string.title_pref_default_do_not_remap), text = stringResource(R.string.summary_pref_default_do_not_remap), - icon = Icons.Rounded.Keyboard, + icon = null, isChecked = state.doNotRemap, onCheckedChange = { callback.onDefaultDoNotRemapChanged(it) }, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index 00989420cd..aaa3b771be 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -44,6 +44,7 @@ import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureTy import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose import io.github.sds100.keymapper.base.utils.ui.compose.SwitchText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe import io.github.sds100.keymapper.system.inputevents.Scancode @@ -110,12 +111,20 @@ fun TriggerKeyOptionsBottomSheet( isCompact = isCompact, ) - SwitchText( - modifier = Modifier.padding(8.dp), - text = stringResource(R.string.flag_dont_override_default_action), + Spacer(Modifier.height(8.dp)) + + SwitchPreferenceCompose( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_default_do_not_remap), + text = stringResource(R.string.summary_pref_default_do_not_remap), + icon = null, isChecked = state.doNotRemapChecked, onCheckedChange = onCheckDoNotRemap, ) + + Spacer(Modifier.height(8.dp)) } if (state is TriggerKeyOptionsState.EvdevEvent) { @@ -131,7 +140,7 @@ fun TriggerKeyOptionsBottomSheet( SwitchText( modifier = Modifier.padding(8.dp), - text = stringResource(R.string.flag_dont_override_default_action), + text = stringResource(R.string.title_pref_default_do_not_remap), isChecked = state.doNotRemapChecked, onCheckedChange = onCheckDoNotRemap, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt index 705ce63916..fc9d72fd22 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt @@ -110,7 +110,8 @@ private fun CompactChipContent( } Text( - text, + modifier = Modifier.weight(1f, fill = false), + text = text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt index ea205c478e..89100155e0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @@ -21,7 +22,7 @@ fun SwitchPreferenceCompose( modifier: Modifier = Modifier, title: String, text: String?, - icon: ImageVector, + icon: ImageVector?, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, isEnabled: Boolean = true, @@ -33,6 +34,7 @@ fun SwitchPreferenceCompose( onCheckedChange(!isChecked) }, enabled = isEnabled, + color = Color.Unspecified, ) { Row( modifier = Modifier @@ -41,11 +43,13 @@ fun SwitchPreferenceCompose( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } Column(modifier = Modifier.weight(1f)) { Text(text = title, style = MaterialTheme.typography.bodyLarge) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index f6713d8da2..92ce7b43e9 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -766,8 +766,8 @@ Change default options For triggers and actions - Do not remap by default - New trigger keys will not consume the key event by default + Passthrough key events + Keep original function of remapped keys Reset all DANGER! @@ -832,7 +832,7 @@ Hold down Hold down until pressed again - Do not remap + Passthrough Copy key map id ⌨ @@ -2128,6 +2128,9 @@ Samsung Auto Blocker prevents Key Mapper from enabling the features it needs on your device. Not working? Samsung Auto Blocker might be turned on. Turn it off before you continue. + Already enabled? + Some Android 17 devices hide whether developer options are on from apps. If you already turned them on, you can skip this step. + Skip Incompatible USB configuration You must select \'No data transfer\' as your default USB configuration so that Expert Mode is not killed every time you lock your device. diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 867f126b5e..db7a58c3b3 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -125,7 +125,8 @@ class PerformActionsUseCaseTest { whenever(mockDisplayAdapter.getDisplay(any())).doReturn( DisplayInfo( id = 0, - activeSize = size, + physicalSize = size, + logicalSize = size, rotation = Orientation.ORIENTATION_0, supportedSizes = setOf(size), ), diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt index 4968595ae9..f0dac26396 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeSetupUseCaseTest.kt @@ -190,4 +190,74 @@ class SystemBridgeSetupUseCaseTest { `is`(SystemBridgeSetupStep.ACCESS_LOCAL_NETWORK_PERMISSION), ) } + + @Test + fun `next step skips DEVELOPER_OPTIONS when wireless debugging is enabled`() = runTest { + val useCase = createUseCaseAfterLocalNetworkStep( + isDeveloperOptionsEnabled = false, + isWirelessDebuggingEnabled = true, + ) + + assertThat(useCase.nextSetupStep.first(), `is`(SystemBridgeSetupStep.WIFI_NETWORK)) + } + + @Test + fun `next step skips DEVELOPER_OPTIONS when the user skips it`() = runTest { + val useCase = createUseCaseAfterLocalNetworkStep( + isDeveloperOptionsEnabled = false, + isWirelessDebuggingEnabled = false, + ) + + assertThat( + useCase.nextSetupStep.first(), + `is`(SystemBridgeSetupStep.DEVELOPER_OPTIONS), + ) + + useCase.skipDeveloperOptionsStep() + + assertThat(useCase.nextSetupStep.first(), `is`(SystemBridgeSetupStep.WIFI_NETWORK)) + } + + /** + * Create a use case where every step before developer options is complete and WiFi + * is not connected. + */ + private fun createUseCaseAfterLocalNetworkStep( + isDeveloperOptionsEnabled: Boolean, + isWirelessDebuggingEnabled: Boolean, + ): SystemBridgeSetupUseCaseImpl { + whenever(mockAccessibilityServiceAdapter.state) + .thenReturn(MutableStateFlow(AccessibilityServiceState.ENABLED)) + whenever(mockPermissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS)) + .thenReturn(flowOf(true)) + whenever(mockPermissionAdapter.isGrantedFlow(Permission.ACCESS_LOCAL_NETWORK)) + .thenReturn(flowOf(true)) + whenever(mockNetworkAdapter.isWifiConnected).thenReturn(flowOf(false)) + whenever(mockSystemBridgeSetupController.isDeveloperOptionsEnabled) + .thenReturn(flowOf(isDeveloperOptionsEnabled)) + whenever(mockSystemBridgeSetupController.isWirelessDebuggingEnabled) + .thenReturn(flowOf(isWirelessDebuggingEnabled)) + whenever(mockSystemBridgeConnectionManager.connectionState) + .thenReturn( + MutableStateFlow( + SystemBridgeConnectionState.Disconnected( + time = 0L, + isStoppedByUser = false, + ), + ), + ) + + return SystemBridgeSetupUseCaseImpl( + preferences = fakePreferences, + suAdapter = mockSuAdapter, + systemBridgeSetupController = mockSystemBridgeSetupController, + systemBridgeConnectionManager = mockSystemBridgeConnectionManager, + shizukuAdapter = mockShizukuAdapter, + permissionAdapter = mockPermissionAdapter, + accessibilityServiceAdapter = mockAccessibilityServiceAdapter, + networkAdapter = mockNetworkAdapter, + packageManagerAdapter = mockPackageManagerAdapter, + clock = mock(), + ) + } } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/SizeKM.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SizeKM.kt index 4903344e83..996eca27bc 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/SizeKM.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SizeKM.kt @@ -24,6 +24,13 @@ data class SizeKM(val width: Int, val height: Int) { return abs(ratio - otherRatio) <= epsilon || abs(ratio - otherRotatedRatio) <= epsilon } + + fun rotate(orientation: Orientation): SizeKM { + return when (orientation) { + Orientation.ORIENTATION_0, Orientation.ORIENTATION_180 -> SizeKM(width, height) + Orientation.ORIENTATION_90, Orientation.ORIENTATION_270 -> SizeKM(height, width) + } + } } val SizeKM.aspectRatio: Float diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 76a9a03883..d4d7c5dcf0 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -34,7 +34,7 @@ lane :production do version_code = get_properties_value(key: "VERSION_CODE", path: "./app/version.properties") version_name = get_properties_value(key: "VERSION_NAME", path: "./app/version.properties") - live_version_codes = google_play_track_version_codes() + live_version_codes = google_play_track_version_codes(track: "internal") latest_live_version_code = live_version_codes.max || 0 if version_code.to_i <= latest_live_version_code diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index a72ccd9640..88662c0d79 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/zh_rCN/full_description.txt b/fastlane/metadata/android/zh_rCN/full_description.txt new file mode 100644 index 0000000000..b6eaa78545 --- /dev/null +++ b/fastlane/metadata/android/zh_rCN/full_description.txt @@ -0,0 +1,55 @@ +给你的键盘或者手柄制作自定义宏,在任意应用中制作屏幕按钮,从你的音量按钮解锁新功能! + +Key Mapper 支持各种各样的按钮和按键*: + +- 你的手机上所有按钮(音量和侧键) +- 游戏控制器(方向键,ABXY 和多数其他) +- 键盘 +- 耳机和头戴式耳机 +- 指纹传感器 + +按键不够?设计你自己的屏幕按钮布局并像真正的按键那样重新映射它们! + + +我可以制作什么快捷方式? +-------------------------- + +超过一百个单独的操作,天空才是极限。 +用屏幕点击和手势、键盘输入、打开应用、控制媒体构建复杂的宏,甚至直接发送 intents 到其他应用。 + + +我能控制多少? +--------------------------- + +触发器:你决定怎样触发一个按键映射。长按,双击,按多少次如你所愿!在不同设备上组合按键,甚至包括你的屏幕按钮。 + +操作:为你想做的事设计特定的宏。组合超过一百个操作,并选择每个之间的延迟。设置重复操作来自动和加速缓慢的任务。 + +约束:你选择按键映射应该什么时候运行,和什么时候不应该运行。仅在一个特定的应用中需要它?或者当媒体正在播放时?在你的锁屏上?为最大的控制约束你的按键映射。 + +* 已支持多数设备,未来会添加新设备。如果它不对你起作用,告诉我们,我们可以优先处理你的设备。 + +当前不支持: + - 鼠标按钮 + - 操纵杆和手柄上的按键(LT,RT) + + +安全和无障碍服务 +--------------------------- + +这个应用包含我们的 Key Mapper 无障碍服务,使用安卓无障碍 API 来检测聚焦的应用,并转发按下按键到用户定义的按键映射。它也可以用来在其他应用上绘制辅助的漂浮按钮覆盖层。 + +接受运行无障碍服务,在你使用你的设备时,本应用会监视键击。它也会模拟滑动和缩放,如果你在本应用中使用这些操作。 + +它不会收集任何用户数据,或者连接到互联网来发送任何数据到任何地方。 + +我们的无障碍服务仅由用户触发,在他们的设备上按下物理按键时。在系统无障碍设置中,用户可以随时关闭它。 + +来我们的 Discord 社区说你好! +keymapper.app/discord + +你自己看代码!(开源) +github.com/keymapperorg/KeyMapper + +阅读文档: +keymapper.app diff --git a/fastlane/metadata/android/zh_rCN/short_description.txt b/fastlane/metadata/android/zh_rCN/short_description.txt new file mode 100644 index 0000000000..be74b90292 --- /dev/null +++ b/fastlane/metadata/android/zh_rCN/short_description.txt @@ -0,0 +1 @@ +为任何东西制作快捷方式!重新映射音量、电源、键盘或者漂浮按钮! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_rCN/title.txt b/fastlane/metadata/android/zh_rCN/title.txt new file mode 100644 index 0000000000..19f819ebd7 --- /dev/null +++ b/fastlane/metadata/android/zh_rCN/title.txt @@ -0,0 +1 @@ +Key Mapper \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt index ff16132065..f892c5b291 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -10,10 +10,13 @@ import java.io.IOException import java.net.InetSocketAddress import java.net.NetworkInterface import java.net.ServerSocket +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update @@ -127,7 +130,7 @@ internal class AdbMdns(ctx: Context, private val serviceType: AdbServiceType) { ) try { - withTimeout(10000L) { + withTimeout(10.seconds) { while (port == null) { val service = serviceDiscoveredChannel?.receive() nsdManager.resolveService(service, resolveListener) @@ -149,6 +152,10 @@ internal class AdbMdns(ctx: Context, private val serviceType: AdbServiceType) { if (isLocalNetwork && isPortAvailable(resolvedService.port)) { Timber.d("Discovered ADB port: ${resolvedService.port}") port = resolvedService.port + } else if (service != null) { + Timber.d("Stale ADB port ${resolvedService.port}, resolving again") + delay(500.milliseconds) + serviceDiscoveredChannel?.trySend(service) } } } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt index 21b524b80f..007501e166 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -152,10 +152,13 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( // doesn't send a death message systemBridge.asBinder().linkToDeath(deathRecipient, 0) - this.systemBridgeFlow.update { systemBridge } - preventSystemBridgeKilling(systemBridge) + // Only expose the system bridge to others once all killing has + // been prevented. Otherwise, there can be race conditions with + // granting or checking the permissions in preventSystemBridgeKilling(). + this.systemBridgeFlow.update { systemBridge } + connectionState.update { SystemBridgeConnectionState.Connected( time = SystemClock.elapsedRealtime(), diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt index 562fb45081..7192330812 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -26,6 +26,7 @@ import io.github.sds100.keymapper.sysbridge.manager.awaitConnected import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl.Companion.START_TIMEOUT_MS import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException @@ -129,12 +130,14 @@ class SystemBridgeSetupControllerImpl @Inject constructor( connectionManager.startWithAdb() // Wait for the service to connect before turning off wireless debugging - withTimeoutOrNull(10000L) { + val isConnected = withTimeoutOrNull(10000L) { connectionManager.awaitConnected() } - // Disable wireless debugging when done - SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 0) + if (isConnected != null && canWriteGlobalSettings()) { + // Disable wireless debugging when done + SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 0) + } } } @@ -199,11 +202,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( SettingsUtils.putGlobalSetting(ctx, DEVELOPER_OPTIONS_SETTING, 1) - try { - withTimeout(5000L) { isDeveloperOptionsEnabled.first { it } } - } catch (_: TimeoutCancellationException) { - return@launch - } + // Do not abort if this times out. Android 17 can redact the developer options + // setting so it always reads as disabled. See issue #2289. + withTimeoutOrNull(5000L.milliseconds) { isDeveloperOptionsEnabled.first { it } } if (isAdbPaired()) { // This is IMPORTANT. First turn on ADB before enabling wireless debugging because @@ -214,7 +215,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( // Wait for wireless debugging to be enabled before starting with ADB try { - withTimeout(5000L) { isWirelessDebuggingEnabled.first { it } } + withTimeout(5000L.milliseconds) { isWirelessDebuggingEnabled.first { it } } } catch (_: TimeoutCancellationException) { return@launch } @@ -222,7 +223,7 @@ class SystemBridgeSetupControllerImpl @Inject constructor( connectionManager.startWithAdb() // Wait for the service to connect before turning off wireless debugging - withTimeoutOrNull(10000L) { + withTimeoutOrNull(10000L.milliseconds) { connectionManager.awaitConnected() } diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt index aac0f1608c..2ec812dc66 100644 --- a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -17,8 +17,10 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.then +import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.sysbridge.BuildConfig import io.github.sds100.keymapper.sysbridge.IShizukuStarterService import io.github.sds100.keymapper.sysbridge.R @@ -71,6 +73,8 @@ class SystemBridgeStarter @Inject constructor( * assuming it failed (e.g. due to the OEM bug on Xiaomi/MediaTek devices). */ private const val SHIZUKU_USER_SERVICE_TIMEOUT_MS = 5000L + + private const val ENABLE_USB_DEBUGGING_ATTEMPTS = 3 } private fun buildShizukuUserServiceArgs(): Shizuku.UserServiceArgs { @@ -227,12 +231,42 @@ class SystemBridgeStarter @Inject constructor( return KMError.Exception(IllegalStateException("User is locked")) } + // See issue #2294. + // Enabling USB debugging on Android 17 QPR1+ resets ADB entirely and kills Shell processes + // even if Wireless Debugging is turned on. Previously, preventSystemBridgeKilling() + // would enable USB debugging AFTER the system bridge has started, but this now + // kills it. So, enable USB debugging before starting the system bridge. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && + + // MUST use ADB to check because adb_enabled is a redacted value on + // some Android 17 builds. See #2289 + !isUsbDebuggingEnabledOverAdb() + ) { + enableUsbDebugging() + } + return startSystemBridgeWithLock(commandExecutor = adbManager::executeCommand) .onFailure { error -> Timber.e("Failed to start system bridge with ADB: $error") } } + private suspend fun isUsbDebuggingEnabledOverAdb(): Boolean { + return adbManager.executeCommand("settings get global adb_enabled").valueOrNull() == "1" + } + + private suspend fun enableUsbDebugging() { + repeat(ENABLE_USB_DEBUGGING_ATTEMPTS) { + // Do not do anything with a failure result. This command + // will kill the ADB process so it may return an error, which is expected. + if (adbManager.executeCommand("settings put global adb_enabled 1").isSuccess) { + return + } + } + + Timber.w("Failed to enable USB debugging over ADB.") + } + suspend fun startWithRoot() { if (Shell.isAppGrantedRoot() != true) { Timber.e("Root is not granted. Cannot start System Bridge with Root.") diff --git a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt index afc4c26d1c..6e5273adca 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt @@ -277,7 +277,12 @@ class AndroidPackageManagerAdapter @Inject constructor( } val pendingIntent = - PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getActivity( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { val bundle = ActivityOptions.makeBasic() 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 818b0741da..88690e60ce 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 @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.graphics.Point import android.hardware.display.DisplayManager import android.os.Build import android.provider.Settings @@ -321,13 +322,18 @@ class AndroidDisplayAdapter @Inject constructor( } private fun buildDisplayInfo(display: Display): DisplayInfo { + val rotation = getDisplayOrientation(display) + val logicalSize = Point().also { display.getRealSize(it) } + return DisplayInfo( id = display.displayId, - activeSize = SizeKM( + physicalSize = SizeKM( width = display.mode.physicalWidth, height = display.mode.physicalHeight, ), - rotation = getDisplayOrientation(display), + // getRealSize is in the current rotation so rotate it back to the natural orientation. + logicalSize = SizeKM(logicalSize.x, logicalSize.y).rotate(rotation), + rotation = rotation, supportedSizes = getSupportedResolutions(display), ) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayInfo.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayInfo.kt index 9eb8a5969c..c30703830f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayInfo.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/DisplayInfo.kt @@ -5,7 +5,15 @@ import io.github.sds100.keymapper.common.utils.SizeKM data class DisplayInfo( val id: Int, - val activeSize: SizeKM, + /** + * The resolution of the current display mode in the natural orientation. + */ + val physicalSize: SizeKM, + /** + * The size in the natural orientation that apps and window coordinates use. This can be + * smaller than [physicalSize] if the user lowered the screen resolution. + */ + val logicalSize: SizeKM, val rotation: Orientation, val supportedSizes: Set, )