diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a586617bf..44584cf50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [4.6.2](https://github.com/sds100/KeyMapper/releases/tag/v4.6.2) + +#### 22 October 2026 + +## Fixed + +- #2069 dpad triggers work reliably again. +- Crash when creating launcher shortcut with empty text because the new text field dialog permitted it. +- Crash navigating to new key map with TriggerSetupShortcut that was not serializable. +- Fix input dispatching time out ANR when dispatching accessibility service gestures. +- Fix crash fetching subgroups if parent group does not exist in database. +- Creating answer and end call does not crash. +- Crash if unable to find app to open Discord server link in Other trigger setup bottom sheet. +- Crash if copying system bridge starter files blocked for too long. +- Do not intentionally crash when a component registered while already registered in InputEventHub. +- Do not crash when testing actions with an error. + ## [4.6.1](https://github.com/sds100/KeyMapper/releases/tag/v4.6.1) #### 10 October 2026 diff --git a/app/version.properties b/app/version.properties index 86b9f20735..da1abaf174 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.6.1 -VERSION_CODE=286 +VERSION_NAME=4.6.2 +VERSION_CODE=288 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index 80be591ffb..2dc932542a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -375,15 +375,13 @@ private fun ExpandedContent( contentDescription = stringResource( R.string.action_list_item_duplicate, ), - tint = MaterialTheme.colorScheme.onSurface, ) } - IconButton(onClick = onTestClick) { + IconButton(onClick = onTestClick, enabled = model.error == null) { Icon( imageVector = Icons.Outlined.PlayArrow, contentDescription = stringResource(R.string.action_list_item_test), - tint = MaterialTheme.colorScheme.onSurface, ) } @@ -391,7 +389,6 @@ private fun ExpandedContent( Icon( imageVector = Icons.Outlined.Settings, contentDescription = stringResource(R.string.action_list_item_edit), - tint = MaterialTheme.colorScheme.onSurface, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt index dbee26bb69..a2813cffc0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt @@ -81,11 +81,11 @@ fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewMod val optionsState by viewModel.actionOptionsState.collectAsStateWithLifecycle() val actionTipModel by viewModel.actionsTip.collectAsStateWithLifecycle() - if (optionsState != null) { + optionsState?.let { optionsState -> ActionOptionsBottomSheet( modifier = Modifier.systemBarsPadding(), sheetState = sheetState, - state = optionsState!!, + state = optionsState, onDismissRequest = { viewModel.actionOptionsUid.update { null } }, callback = viewModel, ) @@ -93,10 +93,10 @@ fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewMod val fixKeyEventActionState by viewModel.fixKeyEventActionState.collectAsStateWithLifecycle() - if (fixKeyEventActionState != null) { + fixKeyEventActionState?.let { fixKeyEventActionState -> FixKeyEventActionBottomSheet( modifier = Modifier.systemBarsPadding(), - state = fixKeyEventActionState!!, + state = fixKeyEventActionState, sheetState = sheetState, onDismissRequest = viewModel::dismissFixKeyEventActionBottomSheet, onEnableAccessibilityServiceClick = viewModel::onEnableAccessibilityServiceClick, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt index 2fb3bd8548..da838adc1a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -51,6 +51,14 @@ class ConfigActionsUseCaseImpl @Inject constructor( keyMap.copy(actionList = newActionList) } + if (data is ActionData.AnswerCall) { + configConstraints.addConstraint(groupUid = null, ConstraintData.PhoneRinging) + } + + if (data is ActionData.EndCall) { + configConstraints.addConstraint(groupUid = null, ConstraintData.InPhoneCall) + } + preferenceRepository.update( Keys.recentlyUsedActions, { old -> @@ -235,7 +243,7 @@ class ConfigActionsUseCaseImpl @Inject constructor( if (data is ActionData.InputKeyEvent) { val containsDpadKey: Boolean = keyMap.trigger.keys - .mapNotNull { it as? KeyEventTriggerKey } + .filterIsInstance() .any { KeyEventUtils.isDpadKeyCode(it.keyCode) } if (KeyEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { @@ -256,14 +264,6 @@ class ConfigActionsUseCaseImpl @Inject constructor( repeat = false } - if (data is ActionData.AnswerCall) { - configConstraints.addConstraint(groupUid = null, ConstraintData.PhoneRinging) - } - - if (data is ActionData.EndCall) { - configConstraints.addConstraint(groupUid = null, ConstraintData.InPhoneCall) - } - return Action( data = data, repeat = repeat, 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 64a5fbacc3..853cd654ed 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 @@ -77,13 +77,17 @@ import io.github.sds100.keymapper.system.volume.VolumeAdapter import io.github.sds100.keymapper.system.volume.VolumeStream import kotlin.math.absoluteValue import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber @@ -140,6 +144,12 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ): PerformActionsUseCaseImpl } + // Use Dispatchers.IO because it dispatching gestures from accessibility services are + // blocking Binder calls. Limit the parallelism to 1 thread so they are dispatched in order. + // This also prevents slow Binder calls that are potentially spammed by the user's key maps + // from starving the thread pool. + private val gestureDispatcher: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1) + private val openMenuHelper by lazy { OpenMenuHelper( service, @@ -292,7 +302,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( switchImeInterface.switchIme(action.imeId) // See issue #1064. Wait for the input method to finish switching before returning. - val chosenIme = withTimeoutOrNull(2000) { + val chosenIme = withTimeoutOrNull(2000.milliseconds) { inputMethodAdapter.chosenIme.filterNotNull().first { it.id == action.imeId } } @@ -312,7 +322,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( switchImeInterface.switchIme(nextIme.id) // See issue #1064. Wait for the input method to finish switching before returning. - val chosenIme = withTimeoutOrNull(2000) { + val chosenIme = withTimeoutOrNull(2000.milliseconds) { inputMethodAdapter.chosenIme.filterNotNull().first { it.id == nextIme.id } } @@ -383,7 +393,9 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( val point = scaleCoordinate(action.x, action.y, action.screenResolution, displaySize) - result = service.tapScreen(point.x, point.y, inputEventAction) + result = withContext(gestureDispatcher) { + service.tapScreen(point.x, point.y, inputEventAction) + } } is ActionData.SwipeScreen -> { @@ -397,15 +409,17 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( val end = scaleCoordinate(action.xEnd, action.yEnd, action.screenResolution, displaySize) - result = service.swipeScreen( - start.x, - start.y, - end.x, - end.y, - action.fingerCount, - action.duration, - inputEventAction, - ) + result = withContext(gestureDispatcher) { + service.swipeScreen( + start.x, + start.y, + end.x, + end.y, + action.fingerCount, + action.duration, + inputEventAction, + ) + } } is ActionData.PinchScreen -> { @@ -415,15 +429,17 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( val distance = scaleDistance(action.distance, action.screenResolution, displaySize) - result = service.pinchScreen( - point.x, - point.y, - distance, - action.pinchType, - action.fingerCount, - action.duration, - inputEventAction, - ) + result = withContext(gestureDispatcher) { + service.pinchScreen( + point.x, + point.y, + distance, + action.pinchType, + action.fingerCount, + action.duration, + inputEventAction, + ) + } } is ActionData.Text -> { @@ -879,7 +895,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( is ActionData.SelectAllText -> { result = service.performActionOnNode({ it.isFocused }) { node -> - val text = node.text?.toString().orEmpty() + val text = node.text.orEmpty() if (text.isEmpty()) return@performActionOnNode null val extras = mapOf( @@ -924,7 +940,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( ) .onSuccess { // Wait 3 seconds so the message isn't shown in the screenshot. - delay(3000) + delay(3000.milliseconds) toastAdapter.show( resourceProvider.getString( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index 7f4af9c185..e91336f87d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -581,46 +581,48 @@ class KeyMapAlgorithm( } } - fun onMotionEvent(event: KMGamePadEvent): Boolean { + /** + * @return whether to consume the [KeyEvent]. + */ + fun onInputEvent(event: KMInputEvent): Boolean { if (!detectKeyMaps) return false - // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad - // Some controllers send motion events as well as key events when DPAD buttons - // are pressed, while others just send key events. - // The motion events must be consumed but this means the following key events are also - // consumed so one must rely on converting these motion events oneself. - - val convertedKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) + when (event) { + is KMKeyEvent -> { + if (dpadMotionEventTracker.onKeyEvent(event)) { + return true + } - var consume = false + val device = event.device - for (keyEvent in convertedKeyEvents) { - if (onKeyEventPostFilter(keyEvent)) { - consume = true + if ((device.isExternal && !detectExternalEvents) || + (!device.isExternal && !detectInternalEvents) + ) { + return false + } } - } - return consume - } + is KMGamePadEvent -> { + // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad + // Some controllers send motion events as well as key events when DPAD buttons + // are pressed, while others just send key events. + // The motion events must be consumed but this means the following key events are also + // consumed so one must rely on converting these motion events oneself. - /** - * @return whether to consume the [KeyEvent]. - */ - fun onInputEvent(event: KMInputEvent): Boolean { - if (!detectKeyMaps) return false + val convertedKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) - if (event is KMKeyEvent) { - if (dpadMotionEventTracker.onKeyEvent(event)) { - return true - } + var consume = false - val device = event.device + for (keyEvent in convertedKeyEvents) { + if (onKeyEventPostFilter(keyEvent)) { + consume = true + } + } - if ((device.isExternal && !detectExternalEvents) || - (!device.isExternal && !detectInternalEvents) - ) { - return false + return consume } + + is KMEvdevEvent -> {} } return onKeyEventPostFilter(event) @@ -1036,8 +1038,10 @@ class KeyMapAlgorithm( val vibrateEffect = when { trigger.vibrate -> resolveVibrateEffect(trigger) + forceVibrate.value -> VibrateEffect.CustomDuration(defaultVibrateDuration.value) + else -> null } 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 90dc1e5f15..e4a9baea83 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 @@ -23,7 +23,9 @@ abstract class SystemBridgeSetupDelegateImpl( ::buildState, ).stateIn( viewModelScope, - SharingStarted.Eagerly, + // Use a timeout so it won't potentially call isAdbPaired when the app + // is in the background. + SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L), State.Loading, ) @@ -34,16 +36,25 @@ abstract class SystemBridgeSetupDelegateImpl( when (currentState) { is ExpertModeSetupState.AccessibilityService -> useCase.enableAccessibilityService() + is ExpertModeSetupState.NotificationPermission -> useCase.requestNotificationPermission() + is ExpertModeSetupState.LocalNetworkPermission -> useCase.requestLocalNetworkPermission() + is ExpertModeSetupState.SamsungAutoBlocker -> useCase.openSamsungAutoBlockerSettings() + is ExpertModeSetupState.DeveloperOptions -> useCase.enableDeveloperOptions() + is ExpertModeSetupState.WifiNetwork -> useCase.connectWifiNetwork() + is ExpertModeSetupState.WirelessDebugging -> useCase.enableWirelessDebugging() + is ExpertModeSetupState.AdbPairing -> useCase.pairWirelessAdb() + is ExpertModeSetupState.StartService -> useCase.startSystemBridgeWithAdb() + is ExpertModeSetupState.Started -> onFinishClick() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/groups/SelectGroupUseCaseImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/groups/SelectGroupUseCaseImpl.kt index e0add1269e..e3e9bcc266 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/groups/SelectGroupUseCaseImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/groups/SelectGroupUseCaseImpl.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.update @OptIn(ExperimentalCoroutinesApi::class) @@ -45,7 +46,11 @@ class SelectGroupUseCaseImpl @Inject constructor(private val groupRepository: Gr } else { val parents = getParentsRecursively(groupUid) - return groupRepository.getGroupWithChildren(groupUid).map { groupWithChildren -> + return groupRepository.getGroupWithChildren(groupUid).mapNotNull { groupWithChildren -> + if (groupWithChildren == null) { + return@mapNotNull null + } + val group = GroupEntityMapper.fromEntity(groupWithChildren.group) val children = groupWithChildren.children.map(GroupEntityMapper::fromEntity) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt index e5f195e56c..e880afe181 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext @@ -79,7 +80,11 @@ class ListKeyMapsUseCaseImpl @Inject constructor( } else { val parents = getParentsRecursively(groupUid) - return groupRepository.getGroupWithChildren(groupUid).map { groupWithChildren -> + return groupRepository.getGroupWithChildren(groupUid).mapNotNull { groupWithChildren -> + if (groupWithChildren == null) { + return@mapNotNull null + } + val group = GroupEntityMapper.fromEntity(groupWithChildren.group) val children = groupWithChildren.children.map(GroupEntityMapper::fromEntity) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt index b1615136e6..4bd0648edc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -209,9 +209,6 @@ class InputEventHubImpl @Inject constructor( evdevEventTypes: List, ) { Timber.d("InputEventHub: Registering client $clientId") - if (clients.containsKey(clientId)) { - throw IllegalArgumentException("This client already has a callback registered!") - } clients[clientId] = ClientContext(callback, emptySet(), evdevEventTypes.toSet()) invalidateGrabbedDevices() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt index 6a55f17cb7..6c5efa1459 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -162,7 +162,7 @@ class ConfigKeyMapOptionsViewModel( } } - suspend fun onConfirmCreateShortcut(name: String): String? { + fun onConfirmCreateShortcut(name: String): String? { val dialogState = createShortcutDialogState ?: return null val result = createKeyMapShortcut.pinShortcut(dialogState.keyMapUid, name, dialogState.icon) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt index 47c1382a66..47197fa1f9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt @@ -76,7 +76,7 @@ fun KeyMapOptionsScreen(modifier: Modifier = Modifier, viewModel: ConfigKeyMapOp title = stringResource(R.string.key_map_options_create_shortcut_title), submitButtonText = stringResource(R.string.pos_save), initialText = createShortcutDialogState.defaultName, - onSubmitClick = { newText -> viewModel.onConfirmCreateShortcut(newText) }, + onSubmitClick = viewModel::onConfirmCreateShortcut, onDismissRequest = viewModel::onDismissCreateShortcutDialog, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index f9a436e41f..9b21f83d20 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -12,8 +12,6 @@ import android.content.res.Configuration import android.graphics.Path import android.graphics.Point import android.os.Build -import android.os.Handler -import android.os.HandlerThread import android.view.KeyEvent import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -185,12 +183,6 @@ abstract class BaseAccessibilityService : abstract fun getController(): BaseAccessibilityServiceController? - /** - * Use a separate thread for dispatching gestures so they do not cause an ANR. - */ - private val gestureHandlerThread: HandlerThread = HandlerThread("gesture_thread") - private var gestureHandler: Handler? = null - override fun onCreate() { super.onCreate() Timber.i("Accessibility service: onCreate") @@ -245,13 +237,6 @@ abstract class BaseAccessibilityService : fingerprintGestureCallback?.let { fingerprintGestureController.registerFingerprintGestureCallback(it, null) } - - try { - gestureHandlerThread.start() - gestureHandler = Handler(gestureHandlerThread.looper) - } catch (_: IllegalThreadStateException) { - // do nothing if onServiceConnected is called again - } } override fun onUnbind(intent: Intent?): Boolean { @@ -264,8 +249,6 @@ abstract class BaseAccessibilityService : override fun onDestroy() { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED - gestureHandlerThread.quit() - fingerprintGestureController .unregisterFingerprintGestureCallback(fingerprintGestureCallback) @@ -401,7 +384,7 @@ abstract class BaseAccessibilityService : addStroke(it) }.build() - val success = dispatchGesture(gestureDescription, null, gestureHandler) + val success = dispatchGesture(gestureDescription, null, null) return if (success) { Success(Unit) @@ -504,7 +487,7 @@ abstract class BaseAccessibilityService : } } - val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) + val success = dispatchGesture(gestureBuilder.build(), null, null) return if (success) { Success(Unit) @@ -557,7 +540,7 @@ abstract class BaseAccessibilityService : gestureBuilder.addStroke(StrokeDescription(p, 0, duration.toLong())) } - val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) + val success = dispatchGesture(gestureBuilder.build(), null, null) return if (success) { Success(Unit) @@ -645,7 +628,7 @@ abstract class BaseAccessibilityService : } override fun performTalkBackGesture(gesture: TalkBackGestureType): KMResult<*> { - return TalkbackGesturePerformer.performTalkBackGesture(this, gesture, gestureHandler) + return TalkbackGesturePerformer.performTalkBackGesture(this, gesture, null) } override fun getActiveDisplayId(): Int { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 40e4b2d6ce..1b0e34e899 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -429,11 +429,7 @@ abstract class BaseAccessibilityServiceController( Timber.d("Service received event from UI: $event") when (event) { - is TestActionEvent -> service.lifecycleScope.launch { - performActionsUseCase.perform( - event.action, - ) - } + is TestActionEvent -> testAction(event) is AccessibilityServiceEvent.Ping -> service.lifecycleScope.launch { outputEvents.emit(AccessibilityServiceEvent.Pong(event.key)) @@ -481,6 +477,16 @@ abstract class BaseAccessibilityServiceController( } } + private fun testAction(event: TestActionEvent) { + service.lifecycleScope.launch { + val errorSnapshot = performActionsUseCase.getErrorSnapshot() + + if (errorSnapshot.getError(event.action) == null) { + performActionsUseCase.perform(event.action) + } + } + } + private fun requestFingerprintGestureDetection() { Timber.d("Accessibility service: request fingerprint gesture detection") serviceFlags.value = diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt index 9530e30cc2..031e056adb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupBottomSheet.kt @@ -641,13 +641,14 @@ private fun OtherTriggerSetupBottomSheet( style = MaterialTheme.typography.bodyMedium, ) + val ctx = LocalContext.current val uriHandler = LocalUriHandler.current val helpUrl = stringResource(R.string.url_discord_server_invite) Button( modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { - uriHandler.openUri(helpUrl) + uriHandler.openUriSafe(ctx, helpUrl) }, colors = ButtonDefaults.buttonColors( containerColor = LocalCustomColorsPalette.current.discord, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupShortcut.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupShortcut.kt index 6d5d7316dc..92cc855563 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupShortcut.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerSetupShortcut.kt @@ -1,7 +1,9 @@ package io.github.sds100.keymapper.base.trigger import androidx.annotation.Keep +import kotlinx.serialization.Serializable +@Serializable @Keep enum class TriggerSetupShortcut { VOLUME, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt index 4e68f0aaaa..b586738ec7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/ComposeDialogs.kt @@ -78,15 +78,19 @@ fun TextFieldDialog( keyboardController?.show() } - val submit = { + val submit: () -> Unit = { scope.launch { + if (!canBeEmpty && textFieldValue.text.isBlank()) { + error = emptyErrorText + return@launch + } + error = onSubmitClick(textFieldValue.text) if (error == null) { onDismissRequest() } } - Unit } CustomDialog( diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt index 16c18a32f1..da7facecfc 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt @@ -110,7 +110,7 @@ class SystemBridgeAutoStarterTest { mockConnectionManager = mock { on { connectionState } doReturn connectionStateFlow - on { canStartSystemBridge() } doReturn true + onBlocking { canStartSystemBridge() } doReturn true } mockSetupController = mock() diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index ed71b52ad4..838cfde5e2 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -1557,19 +1557,19 @@ class KeyMapAlgorithmTest { ) var motionEvent = createMotionEvent(axisHatY = 1.0f) - val consumeDown1 = controller.onMotionEvent(motionEvent) + val consumeDown1 = controller.onInputEvent(motionEvent) assertThat(consumeDown1, `is`(false)) motionEvent = motionEvent.copy(axisHatX = -1.0f) - val consumeDown2 = controller.onMotionEvent(motionEvent) + val consumeDown2 = controller.onInputEvent(motionEvent) assertThat(consumeDown2, `is`(true)) motionEvent = motionEvent.copy(axisHatX = 0.0f) - val consumeUp2 = controller.onMotionEvent(motionEvent) + val consumeUp2 = controller.onInputEvent(motionEvent) assertThat(consumeUp2, `is`(true)) motionEvent = motionEvent.copy(axisHatY = 0.0f) - val consumeUp1 = controller.onMotionEvent(motionEvent) + val consumeUp1 = controller.onInputEvent(motionEvent) assertThat(consumeUp1, `is`(false)) verify(performActionsUseCase, times(1)).perform( @@ -5253,7 +5253,7 @@ class KeyMapAlgorithmTest { axisHatX: Float = 0.0f, axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, - ): Boolean = controller.onMotionEvent( + ): Boolean = controller.onInputEvent( KMGamePadEvent( metaState = 0, device = device, diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/DisplayUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/DisplayUtils.kt index 6f0f332b70..cdddfdda7a 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/DisplayUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/DisplayUtils.kt @@ -1,10 +1,7 @@ package io.github.sds100.keymapper.common.utils import android.content.Context -import android.graphics.Point -import android.util.Size import android.util.TypedValue -import androidx.core.content.ContextCompat fun dpToPx(context: Context, dp: Float): Float { return TypedValue.applyDimension( @@ -13,23 +10,3 @@ fun dpToPx(context: Context, dp: Float): Float { context.resources.displayMetrics, ) } - -fun Context.getDisplayDensity(): Float { - return resources.displayMetrics.density -} - -fun Context.getRealDisplaySize(): SizeKM { - val point = Point().apply { - ContextCompat.getDisplayOrDefault(this@getRealDisplaySize).getRealSize(this) - } - - return SizeKM(point.x, point.y) -} - -fun Context.getApplicationDisplaySize(): Size { - val point = Point().apply { - ContextCompat.getDisplayOrDefault(this@getApplicationDisplaySize).getSize(this) - } - - return Size(point.x, point.y) -} diff --git a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/GroupDao.kt b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/GroupDao.kt index 1fa1a15137..3bc9d07856 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/db/dao/GroupDao.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/db/dao/GroupDao.kt @@ -8,7 +8,6 @@ import androidx.room.Query import androidx.room.Update import io.github.sds100.keymapper.data.entities.GroupEntity import io.github.sds100.keymapper.data.entities.GroupEntityWithChildren -import io.github.sds100.keymapper.data.entities.KeyMapEntitiesWithGroup import kotlinx.coroutines.flow.Flow @Dao @@ -26,9 +25,6 @@ interface GroupDao { @Query("SELECT * FROM $TABLE_NAME") fun getAll(): Flow> - @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_UID = (:groupUid)") - fun getKeyMapsByGroup(groupUid: String): Flow - @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_UID = (:uid)") fun getById(uid: String): GroupEntity? @@ -39,7 +35,7 @@ interface GroupDao { fun getByIdFlow(uid: String): Flow @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_UID = (:uid)") - fun getGroupWithSubGroups(uid: String): Flow + fun getGroupWithSubGroups(uid: String): Flow @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_PARENT_UID IS (:uid)") fun getGroupsByParent(uid: String?): Flow> diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyMapEntitiesWithGroup.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyMapEntitiesWithGroup.kt deleted file mode 100644 index 7c6a12a082..0000000000 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyMapEntitiesWithGroup.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.sds100.keymapper.data.entities - -import android.os.Parcelable -import androidx.room.Embedded -import androidx.room.Relation -import io.github.sds100.keymapper.data.db.dao.GroupDao -import io.github.sds100.keymapper.data.db.dao.KeyMapDao -import kotlinx.parcelize.Parcelize - -@Parcelize -data class KeyMapEntitiesWithGroup( - @Embedded - val group: GroupEntity, - - @Relation( - parentColumn = GroupDao.KEY_UID, - entityColumn = KeyMapDao.KEY_GROUP_UID, - ) - val keyMaps: List, -) : Parcelable diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/GroupRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/GroupRepository.kt index ec00f476d7..109184204c 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/GroupRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/GroupRepository.kt @@ -2,18 +2,16 @@ package io.github.sds100.keymapper.data.repositories import io.github.sds100.keymapper.data.entities.GroupEntity import io.github.sds100.keymapper.data.entities.GroupEntityWithChildren -import io.github.sds100.keymapper.data.entities.KeyMapEntitiesWithGroup import kotlinx.coroutines.flow.Flow interface GroupRepository { val groups: Flow> - fun getKeyMapsByGroup(groupUid: String): Flow suspend fun getGroup(uid: String): GroupEntity? fun getAllGroups(): Flow> fun getGroups(vararg uid: String): Flow> fun getGroupsByParent(uid: String?): Flow> - fun getGroupWithChildren(uid: String): Flow + fun getGroupWithChildren(uid: String): Flow suspend fun insert(groupEntity: GroupEntity) suspend fun update(groupEntity: GroupEntity) fun delete(uid: String) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomGroupRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomGroupRepository.kt index 5e4b86bf3d..f28dc8502c 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomGroupRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomGroupRepository.kt @@ -5,7 +5,6 @@ import io.github.sds100.keymapper.common.utils.DispatcherProvider import io.github.sds100.keymapper.data.db.dao.GroupDao import io.github.sds100.keymapper.data.entities.GroupEntity import io.github.sds100.keymapper.data.entities.GroupEntityWithChildren -import io.github.sds100.keymapper.data.entities.KeyMapEntitiesWithGroup import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -27,10 +26,6 @@ class RoomGroupRepository @Inject constructor( override val groups: StateFlow> = dao.getAll().stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), emptyList()) - override fun getKeyMapsByGroup(groupUid: String): Flow { - return dao.getKeyMapsByGroup(groupUid).flowOn(dispatchers.io()) - } - override suspend fun getGroup(uid: String): GroupEntity? { return withContext(dispatchers.io()) { dao.getById(uid) } } @@ -47,7 +42,7 @@ class RoomGroupRepository @Inject constructor( return dao.getGroupsByParent(uid).flowOn(dispatchers.io()) } - override fun getGroupWithChildren(uid: String): Flow { + override fun getGroupWithChildren(uid: String): Flow { return dao.getGroupWithSubGroups(uid).flowOn(dispatchers.io()) } diff --git a/fastlane/metadata/android/zh_rCN/full_description.txt b/fastlane/metadata/android/zh_CN/full_description.txt similarity index 100% rename from fastlane/metadata/android/zh_rCN/full_description.txt rename to fastlane/metadata/android/zh_CN/full_description.txt diff --git a/fastlane/metadata/android/zh_rCN/short_description.txt b/fastlane/metadata/android/zh_CN/short_description.txt similarity index 100% rename from fastlane/metadata/android/zh_rCN/short_description.txt rename to fastlane/metadata/android/zh_CN/short_description.txt diff --git a/fastlane/metadata/android/zh_rCN/title.txt b/fastlane/metadata/android/zh_CN/title.txt similarity index 100% rename from fastlane/metadata/android/zh_rCN/title.txt rename to fastlane/metadata/android/zh_CN/title.txt 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 007501e166..b70d157c50 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 @@ -69,7 +69,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) override val connectionState: MutableStateFlow = - MutableStateFlow( + MutableStateFlow( SystemBridgeConnectionState.Disconnected( time = SystemClock.elapsedRealtime(), // Get whether the user previously stopped the system bridge. @@ -251,12 +251,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( // the app process and restart it. This is normal, expected behavior and can not be // worked around. Do not grant any other permissions automatically here. - val isWriteSecureSettingsGranted = ContextCompat.checkSelfPermission( - ctx, - Manifest.permission.WRITE_SECURE_SETTINGS, - ) == PERMISSION_GRANTED - - if (!isWriteSecureSettingsGranted) { + if (!isWriteSecureSettingsGranted()) { grantWriteSecureSettings(systemBridge) } @@ -264,7 +259,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( // Only turn on the ADB options to prevent killing if it is running under // the ADB shell user - if (isWriteSecureSettingsGranted && isShellProcess) { + if (isShellProcess && isWriteSecureSettingsGranted()) { // Disable automatic revoking of ADB pairings SettingsUtils.putGlobalSetting( ctx, @@ -284,11 +279,16 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( val isMiuiOptimisationEnabled = SettingsUtils.getGlobalSetting(ctx, MIUI_OPTIMIZATION_SETTING) == 1 - if (isWriteSecureSettingsGranted && isMiuiOptimisationEnabled) { + if (isMiuiOptimisationEnabled && isWriteSecureSettingsGranted()) { SettingsUtils.putGlobalSetting(ctx, MIUI_OPTIMIZATION_SETTING, 0) } } + private fun isWriteSecureSettingsGranted(): Boolean = ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.WRITE_SECURE_SETTINGS, + ) == PERMISSION_GRANTED + private fun grantWriteSecureSettings(systemBridge: ISystemBridge) { val deviceId: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { @@ -317,7 +317,7 @@ class SystemBridgeConnectionManagerImpl @Inject constructor( return starter.getStartCommand() } - override fun canStartSystemBridge(): Boolean { + override suspend fun canStartSystemBridge(): Boolean { return starter.canStartSystemBridge() } } @@ -346,7 +346,7 @@ interface SystemBridgeConnectionManager { * nowhere to copy the starter files to because the shared storage is unavailable, which can * happen at any point while the device is running. */ - fun canStartSystemBridge(): Boolean + suspend fun canStartSystemBridge(): Boolean } fun SystemBridgeConnectionManager.isConnected(): Boolean { 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 7192330812..2fe0c08a96 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 @@ -19,6 +19,7 @@ import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.isSuccess import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.sysbridge.adb.AdbManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState @@ -56,7 +57,9 @@ class SystemBridgeSetupControllerImpl @Inject constructor( companion object { private const val DEVELOPER_OPTIONS_SETTING = "development_settings_enabled" private const val ADB_WIRELESS_SETTING = "adb_wifi_enabled" + private const val ADB_USB_SETTING = "adb_enabled" private const val START_TIMEOUT_MS = 60_000L + private const val ENABLE_USB_DEBUGGING_ATTEMPTS = 3 } private val activityManager: ActivityManager by lazy { ctx.getSystemService()!! } @@ -127,6 +130,20 @@ class SystemBridgeSetupControllerImpl @Inject constructor( @RequiresApi(Build.VERSION_CODES.R) override fun startWithAdb() { launchStartJob { + // 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() + ) { + enableUsbDebuggingWithAdb() + } + connectionManager.startWithAdb() // Wait for the service to connect before turning off wireless debugging @@ -286,6 +303,25 @@ class SystemBridgeSetupControllerImpl @Inject constructor( } } + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun isUsbDebuggingEnabledOverAdb(): Boolean { + return adbManager.executeCommand("settings get global $ADB_USB_SETTING") + .valueOrNull() == "1" + } + + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun enableUsbDebuggingWithAdb() { + 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_USB_SETTING 1").isSuccess) { + return + } + } + + Timber.w("Failed to enable USB debugging over ADB.") + } + @RequiresApi(Build.VERSION_CODES.R) override suspend fun isAdbPaired(): Boolean { // Sometimes multiple calls to this function can happen in a short space of time 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 2ec812dc66..c8a52b9bce 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,10 +17,8 @@ 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 @@ -73,8 +71,6 @@ 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 { @@ -231,42 +227,12 @@ 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.") @@ -306,7 +272,7 @@ class SystemBridgeStarter @Inject constructor( * nowhere to copy the starter files to because the shared storage is unavailable, which can * happen at any point while the device is running. */ - fun canStartSystemBridge(): Boolean { + suspend fun canStartSystemBridge(): Boolean { return getStarterFilesDirectory() != null } @@ -329,9 +295,9 @@ class SystemBridgeStarter @Inject constructor( * getExternalFilesDir returns null while the shared storage is not mounted, which can happen * at any point while the device is running and usually resolves itself within a minute. */ - private fun getStarterFilesDirectory(): File? { + private suspend fun getStarterFilesDirectory(): File? = withContext(Dispatchers.IO) { if (buildConfigProvider.sdkInt > Build.VERSION_CODES.R) { - return try { + return@withContext try { ctx.getExternalFilesDir(null)?.parentFile } catch (e: IOException) { null @@ -349,7 +315,7 @@ class SystemBridgeStarter @Inject constructor( e.printStackTrace() } - return protectedStorageDir + return@withContext protectedStorageDir } /**