From 6fd3c180f284c89ccccada32de75be541b982326 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 1 Apr 2025 23:48:20 -0600 Subject: [PATCH 01/69] #257 create accessibility node repository and WIP recording of nodes --- .../github/sds100/keymapper/ServiceLocator.kt | 15 +++ .../data/entities/AccessibilityNodeEntity.kt | 16 +++ .../AccessibilityNodeRepository.kt | 34 +++++++ .../AccessibilityNodeRecorder.kt | 74 ++++++++++++++ .../BaseAccessibilityServiceController.kt | 99 +++++++++++++------ .../accessibility/MyAccessibilityService.kt | 13 ++- .../sds100/keymapper/util/ServiceEvent.kt | 6 ++ 7 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt index 00f14520f2..342bd175ce 100755 --- a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt @@ -8,6 +8,8 @@ import io.github.sds100.keymapper.actions.sound.SoundsManagerImpl import io.github.sds100.keymapper.backup.BackupManager import io.github.sds100.keymapper.backup.BackupManagerImpl import io.github.sds100.keymapper.data.db.AppDatabase +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepositoryImpl import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository import io.github.sds100.keymapper.data.repositories.GroupRepository @@ -214,6 +216,19 @@ object ServiceLocator { ) } + @Volatile + private var accessibilityNodeRepository: AccessibilityNodeRepository? = null + + fun accessibilityNodeRepository(context: Context): AccessibilityNodeRepository { + synchronized(this) { + return accessibilityNodeRepository ?: AccessibilityNodeRepositoryImpl( + (context.applicationContext as KeyMapperApp).appCoroutineScope, + ).also { + this.accessibilityNodeRepository = it + } + } + } + fun fileAdapter(context: Context): FileAdapter = (context.applicationContext as KeyMapperApp).fileAdapter fun inputMethodAdapter(context: Context): InputMethodAdapter = (context.applicationContext as KeyMapperApp).inputMethodAdapter diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt new file mode 100644 index 0000000000..863983b572 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.data.entities + +data class AccessibilityNodeEntity( + val id: Long = 0L, + val parentId: Long? = null, + val packageName: String, + val text: String?, + val contentDescription: String?, + val className: String?, + val viewResourceId: String?, + val uniqueId: String?, + /** + * A list of the allowed accessibility node actions. + */ + val actions: List, +) diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt new file mode 100644 index 0000000000..b2fa1e522f --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt @@ -0,0 +1,34 @@ +package io.github.sds100.keymapper.data.repositories + +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity +import io.github.sds100.keymapper.util.State +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +interface AccessibilityNodeRepository { + val nodes: Flow>> + fun insert(vararg node: AccessibilityNodeEntity) + fun deleteAll() +} + +class AccessibilityNodeRepositoryImpl(private val coroutineScope: CoroutineScope) : AccessibilityNodeRepository { + override val nodes = + MutableStateFlow>>(State.Data(ArrayList(128))) + + override fun insert(vararg node: AccessibilityNodeEntity) { + coroutineScope.launch { + val currentState = nodes.value + if (currentState is State.Data) { + nodes.emit(State.Data(currentState.data.plus(node))) + } + } + } + + override fun deleteAll() { + coroutineScope.launch { + nodes.emit(State.Data(ArrayList(128))) + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt new file mode 100644 index 0000000000..71cb54bcf2 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -0,0 +1,74 @@ +package io.github.sds100.keymapper.system.accessibility + +import android.accessibilityservice.AccessibilityService +import android.os.Build +import android.view.accessibility.AccessibilityEvent +import io.github.sds100.keymapper.ServiceLocator +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class AccessibilityNodeRecorder( + private val service: AccessibilityService, + private val coroutineScope: CoroutineScope, +) { + companion object { + private const val RECORD_DURATION = 60000L + } + + private val nodeRepository: AccessibilityNodeRepository by lazy { + ServiceLocator.accessibilityNodeRepository(service) + } + + private var recordJob: Job? = null + private val _isRecording = MutableStateFlow(false) + val isRecording = _isRecording.asStateFlow() + + fun startRecording() { + _isRecording.update { true } + recordJob?.cancel() + recordJob = recordJob() + } + + fun stopRecording() { + recordJob?.cancel() + recordJob = null + _isRecording.update { false } + } + + fun onAccessibilityEvent(event: AccessibilityEvent) { + if (!isRecording.value) { + return + } + + val source = event.source ?: return + + val entity = + AccessibilityNodeEntity( + packageName = event.packageName.toString(), + text = source.text.firstOrNull()?.toString(), + contentDescription = source.contentDescription?.toString(), + className = source.className?.toString(), + viewResourceId = source.viewIdResourceName, + uniqueId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + source.uniqueId + } else { + null + }, + actions = source.actionList?.map { it.id } ?: emptyList(), + ) + + nodeRepository.insert(entity) + } + + private fun recordJob() = coroutineScope.launch { + delay(RECORD_DURATION) + _isRecording.update { false } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index 6da22df6af..676c6ea7ee 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -60,7 +60,7 @@ import timber.log.Timber */ abstract class BaseAccessibilityServiceController( private val coroutineScope: CoroutineScope, - private val accessibilityService: IAccessibilityService, + private val service: MyAccessibilityService, private val inputEvents: SharedFlow, private val outputEvents: MutableSharedFlow, private val detectConstraintsUseCase: DetectConstraintsUseCase, @@ -101,6 +101,9 @@ abstract class BaseAccessibilityServiceController( rerouteKeyEventsUseCase, ) + private val accessibilityNodeRecorder: AccessibilityNodeRecorder = + AccessibilityNodeRecorder(service, coroutineScope) + private var recordingTriggerJob: Job? = null private val recordingTrigger: Boolean get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true @@ -114,7 +117,7 @@ abstract class BaseAccessibilityServiceController( detectKeyMapsUseCase.detectScreenOffTriggers .stateIn(coroutineScope, SharingStarted.Eagerly, false) - private val changeImeOnInputFocus: StateFlow = + private val changeImeOnInputFocusFlow: StateFlow = settingsRepository .get(Keys.changeImeOnInputFocus) .map { it ?: PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS } @@ -145,6 +148,7 @@ abstract class BaseAccessibilityServiceController( // This is required for receive TYPE_WINDOWS_CHANGED events so can // detect when to show/hide overlays. .withFlag(AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS) + .withFlag(AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { flags = flags.withFlag(AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME) @@ -171,24 +175,25 @@ abstract class BaseAccessibilityServiceController( MutableStateFlow(AccessibilityEvent.TYPE_WINDOWS_CHANGED) init { + serviceFlags.onEach { flags -> // check that it isn't null because this can only be called once the service is bound - if (accessibilityService.serviceFlags != null) { - accessibilityService.serviceFlags = flags + if (service.serviceFlags != null) { + service.serviceFlags = flags } }.launchIn(coroutineScope) serviceFeedbackType.onEach { feedbackType -> // check that it isn't null because this can only be called once the service is bound - if (accessibilityService.serviceFeedbackType != null) { - accessibilityService.serviceFeedbackType = feedbackType + if (service.serviceFeedbackType != null) { + service.serviceFeedbackType = feedbackType } }.launchIn(coroutineScope) serviceEventTypes.onEach { eventTypes -> // check that it isn't null because this can only be called once the service is bound - if (accessibilityService.serviceEventTypes != null) { - accessibilityService.serviceEventTypes = eventTypes + if (service.serviceEventTypes != null) { + service.serviceEventTypes = eventTypes } }.launchIn(coroutineScope) @@ -224,7 +229,7 @@ abstract class BaseAccessibilityServiceController( onEventFromUi(it) }.launchIn(coroutineScope) - accessibilityService.isKeyboardHidden + service.isKeyboardHidden .drop(1) // Don't send it when collecting initially .onEach { isHidden -> if (isHidden) { @@ -255,23 +260,44 @@ abstract class BaseAccessibilityServiceController( } }.launchIn(coroutineScope) - changeImeOnInputFocus.onEach { changeImeOnInputFocus -> - if (changeImeOnInputFocus) { - serviceEventTypes.value = serviceEventTypes.value - .withFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) - .withFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) - } else { - serviceEventTypes.value = serviceEventTypes.value - .minusFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) - .minusFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) - } - }.launchIn(coroutineScope) + val imeInputFocusEvents = + AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED + val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or + AccessibilityEvent.TYPE_VIEW_CLICKED or + AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + +// coroutineScope.launch { +// combine( +// changeImeOnInputFocusFlow, +// accessibilityNodeRecorder.isRecording +// ) { changeImeOnInputFocus, isRecordingNodes -> +// serviceEventTypes.update { eventTypes -> +// var newEventTypes = eventTypes +// +// +// +// newEventTypes +// } +// +// }.collect() +// } +// changeImeOnInputFocusFlow.onEach { changeImeOnInputFocus -> +// if (changeImeOnInputFocus) { +// serviceEventTypes.value = serviceEventTypes.value +// .withFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) +// .withFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) +// } else { +// serviceEventTypes.value = serviceEventTypes.value +// .minusFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) +// .minusFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) +// } +// }.launchIn(coroutineScope) } open fun onServiceConnected() { - accessibilityService.serviceFlags = serviceFlags.value - accessibilityService.serviceFeedbackType = serviceFeedbackType.value - accessibilityService.serviceEventTypes = serviceEventTypes.value + service.serviceFlags = serviceFlags.value + service.serviceFeedbackType = serviceFeedbackType.value + service.serviceEventTypes = serviceEventTypes.value // check if fingerprint gestures are supported if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -284,7 +310,7 @@ abstract class BaseAccessibilityServiceController( * used while this is called. */ if (fingerprintGesturesSupported.isSupported.firstBlocking() != true) { fingerprintGesturesSupported.setSupported( - accessibilityService.isFingerprintGestureDetectionAvailable, + service.isFingerprintGestureDetectionAvailable, ) } @@ -441,10 +467,12 @@ abstract class BaseAccessibilityServiceController( } } - open fun onAccessibilityEvent(event: AccessibilityEventModel) { - if (changeImeOnInputFocus.value) { + open fun onAccessibilityEvent(event: AccessibilityEvent) { + accessibilityNodeRecorder.onAccessibilityEvent(event) + + if (changeImeOnInputFocusFlow.value) { val focussedNode = - accessibilityService.findFocussedNode(AccessibilityNodeInfo.FOCUS_INPUT) + service.findFocussedNode(AccessibilityNodeInfo.FOCUS_INPUT) if (focussedNode?.isEditable == true && focussedNode.isFocused) { Timber.d("Got input focus") @@ -497,14 +525,23 @@ abstract class BaseAccessibilityServiceController( outputEvents.emit(ServiceEvent.Pong(event.key)) } - is ServiceEvent.HideKeyboard -> accessibilityService.hideKeyboard() - is ServiceEvent.ShowKeyboard -> accessibilityService.showKeyboard() - is ServiceEvent.ChangeIme -> accessibilityService.switchIme(event.imeId) + is ServiceEvent.HideKeyboard -> service.hideKeyboard() + is ServiceEvent.ShowKeyboard -> service.showKeyboard() + is ServiceEvent.ChangeIme -> service.switchIme(event.imeId) is ServiceEvent.DisableService -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - accessibilityService.disableSelf() + service.disableSelf() } is ServiceEvent.TriggerKeyMap -> triggerKeyMapFromIntent(event.uid) + + is ServiceEvent.StartRecordingNodes -> { + accessibilityNodeRecorder.startRecording() + } + + is ServiceEvent.StopRecordingNodes -> { + accessibilityNodeRecorder.stopRecording() + } + else -> Unit } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index b909f9ae6f..836166c4e5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -21,7 +21,9 @@ import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner +import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.ServiceLocator import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.api.KeyEventRelayService @@ -37,6 +39,7 @@ import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.MathUtils import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success +import io.github.sds100.keymapper.util.onSuccess import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -187,6 +190,14 @@ class MyAccessibilityService : override fun onServiceConnected() { super.onServiceConnected() + val inputMethodAdapter = ServiceLocator.inputMethodAdapter(this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + inputMethodAdapter.getInfoByPackageName(Constants.PACKAGE_NAME).onSuccess { + softKeyboardController.setInputMethodEnabled(it.id, true) + softKeyboardController.switchToInputMethod(it.id) + } + } Timber.i("Accessibility service: onServiceConnected") lifecycleRegistry.currentState = Lifecycle.State.STARTED @@ -281,7 +292,7 @@ class MyAccessibilityService : _activeWindowPackage.update { rootInActiveWindow?.packageName?.toString() } } - controller?.onAccessibilityEvent(event.toModel()) + controller?.onAccessibilityEvent(event) } override fun onKeyEvent(event: KeyEvent?): Boolean { diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt index 4db23d48bb..20777f5e4f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt @@ -69,4 +69,10 @@ sealed class ServiceEvent { @Serializable data class TriggerKeyMap(val uid: String) : ServiceEvent() + + @Serializable + data object StartRecordingNodes : ServiceEvent() + + @Serializable + data object StopRecordingNodes : ServiceEvent() } From 4660c001e17011d5555aafebdb3db2ba6b4bfc0d Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 1 Apr 2025 23:53:48 -0600 Subject: [PATCH 02/69] #257 add field for whether the user interacted with the node --- .../keymapper/data/entities/AccessibilityNodeEntity.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index 863983b572..f480ef56e6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -13,4 +13,10 @@ data class AccessibilityNodeEntity( * A list of the allowed accessibility node actions. */ val actions: List, + /** + * The accessibility action id of how the user interacted + * with this node. This is null if the user didn't interact with + * this node. + */ + val userInteractedActionId: Int?, ) From 4a30fdc0aae5ca7f978ae60b3b6c5252898adb10 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 14 Apr 2025 13:13:56 +0200 Subject: [PATCH 03/69] #699 WIP: time constraints --- .../actions/FlashlightActionBottomSheet.kt | 2 + .../constraints/ChooseConstraintViewModel.kt | 18 ++ .../keymapper/constraints/Constraint.kt | 33 ++++ .../keymapper/constraints/ConstraintId.kt | 2 + .../constraints/TimeConstraintBottomSheet.kt | 166 ++++++++++++++++++ .../data/entities/ConstraintEntity.kt | 8 + app/src/main/res/values/strings.xml | 6 + 7 files changed, 235 insertions(+) create mode 100644 app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt index 1482dd5327..346dc1114b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt @@ -35,7 +35,9 @@ import androidx.compose.material3.SheetValue import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt index 04a6070f6a..e1d066d326 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt @@ -104,6 +104,15 @@ class ChooseConstraintViewModel( State.Data(filteredItems) }.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + var timeConstraintState: Constraint.Time? = null + + fun onDoneConfigTimeConstraintClick() { + timeConstraintState?.let { constraint -> + _returnResult.tryEmit(constraint) + timeConstraintState = null + } + } + fun onListItemClick(id: String) { viewModelScope.launch { when (val constraintType = ConstraintId.valueOf(id)) { @@ -192,6 +201,15 @@ class ChooseConstraintViewModel( ConstraintId.LOCK_SCREEN_NOT_SHOWING -> _returnResult.emit(Constraint.LockScreenNotShowing()) + + ConstraintId.TIME -> { + timeConstraintState = Constraint.Time( + startHour = 0, + startMinute = 0, + endHour = 0, + endMinute = 0, + ) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt index 16f3726d85..9633d38cf6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt @@ -216,6 +216,17 @@ sealed class Constraint { data class Discharging(override val uid: String = UUID.randomUUID().toString()) : Constraint() { override val id: ConstraintId = ConstraintId.DISCHARGING } + + @Serializable + data class Time( + override val uid: String = UUID.randomUUID().toString(), + val startHour: Int, + val startMinute: Int, + val endHour: Int, + val endMinute: Int, + ) : Constraint() { + override val id: ConstraintId = ConstraintId.TIME + } } object ConstraintModeEntityMapper { @@ -375,6 +386,28 @@ object ConstraintEntityMapper { ConstraintEntity.CHARGING -> Constraint.Charging(uid = entity.uid) ConstraintEntity.DISCHARGING -> Constraint.Discharging(uid = entity.uid) + ConstraintEntity.TIME -> { + val startTime = + entity.extras.getData(ConstraintEntity.EXTRA_START_TIME).valueOrNull()!! + .split(":") + val startHour = startTime[0].toInt() + val startMin = startTime[1].toInt() + + val endTime = + entity.extras.getData(ConstraintEntity.EXTRA_END_TIME).valueOrNull()!! + .split(":") + val endHour = endTime[0].toInt() + val endMin = endTime[1].toInt() + + Constraint.Time( + uid = entity.uid, + startHour = startHour, + startMinute = startMin, + endHour = endHour, + endMinute = endMin, + ) + } + else -> throw Exception("don't know how to convert constraint entity with type ${entity.type}") } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt index 72f347cb3d..0c7f2678ae 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt @@ -48,4 +48,6 @@ enum class ConstraintId { CHARGING, DISCHARGING, + + TIME } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt new file mode 100644 index 0000000000..ce2e92d6d8 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt @@ -0,0 +1,166 @@ +package io.github.sds100.keymapper.constraints + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.width +import androidx.compose.material.icons.Icons +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimeConstraintBottomSheet(viewModel: ChooseConstraintViewModel) { + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + if (viewModel.timeConstraintState != null) { + TimeConstraintBottomSheet( + sheetState = sheetState, + onDismissRequest = { + viewModel.timeConstraintState = null + }, + state = viewModel.timeConstraintState!!, + onSelectStartTime = { hour, min -> + viewModel.timeConstraintState = viewModel.timeConstraintState?.copy( + startHour = hour, + startMinute = min, + ) + }, + onSelectEndTime = { hour, min -> + viewModel.timeConstraintState = viewModel.timeConstraintState?.copy( + endHour = hour, + endMinute = min, + ) + }, + onDoneClick = { + scope.launch { + sheetState.hide() + viewModel.onDoneConfigTimeConstraintClick() + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TimeConstraintBottomSheet( + sheetState: SheetState, + onDismissRequest: () -> Unit, + state: Constraint.Time, + onSelectStartTime: (Int, Int) -> Unit = { _, _ -> }, + onSelectEndTime: (Int, Int) -> Unit = { _, _ -> }, + onDoneClick: () -> Unit = {}, +) { + val scope = rememberCoroutineScope() + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + dragHandle = null, + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + textAlign = TextAlign.Center, + text = stringResource(R.string.constraint_time_bottom_sheet_title), + style = MaterialTheme.typography.headlineMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + icon = Icons.Rounded.Clo + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Button( + modifier = Modifier.weight(1f), + onClick = onDoneClick, + ) { + Text(stringResource(R.string.pos_done)) + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = SheetValue.Expanded, + ) + + TimeConstraintBottomSheet( + sheetState = sheetState, + onDismissRequest = {}, + state = Constraint.Time( + startHour = 0, + startMinute = 0, + endHour = 23, + endMinute = 59, + ), + onSelectStartTime = { _, _ -> }, + onSelectEndTime = { _, _ -> }, + onDoneClick = {}, + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index f35832c726..c79ea9cb66 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -85,6 +85,8 @@ data class ConstraintEntity( const val CHARGING = "charging" const val DISCHARGING = "discharging" + const val TIME = "time" + const val EXTRA_PACKAGE_NAME = "extra_package_name" const val EXTRA_BT_ADDRESS = "extra_bluetooth_device_address" const val EXTRA_BT_NAME = "extra_bluetooth_device_name" @@ -93,6 +95,12 @@ data class ConstraintEntity( const val EXTRA_IME_ID = "extra_ime_id" const val EXTRA_IME_LABEL = "extra_ime_label" + /** + * The time is stored in the following format: 20:25. + */ + const val EXTRA_START_TIME = "extra_start_time" + const val EXTRA_END_TIME = "extra_end_time" + val DESERIALIZER = jsonDeserializer { val type by it.json.byString(NAME_TYPE) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dfd8e9db02..323a3c35a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -288,6 +288,12 @@ Landscape (90°) Portrait (180°) Landscape (270°) + + Time + Time between %s and %s + Time constraint + Start time + End time From 67e1df107b9904ddf583f895538db1647120525b Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 29 Apr 2025 12:46:12 +0200 Subject: [PATCH 04/69] #699 feat: time constraints --- app/build.gradle | 4 + .../constraints/ChooseConstraintScreen.kt | 2 + .../constraints/ChooseConstraintViewModel.kt | 13 +- .../keymapper/constraints/Constraint.kt | 17 +++ .../constraints/ConstraintSnapshot.kt | 10 ++ .../constraints/ConstraintUiHelper.kt | 11 ++ .../keymapper/constraints/ConstraintUtils.kt | 3 + .../constraints/TimeConstraintBottomSheet.kt | 134 +++++++++++++++++- .../KeyMapConstraintsComparator.kt | 7 + .../github/sds100/keymapper/util/TimeUtils.kt | 11 ++ app/src/main/res/values/strings.xml | 2 + .../keymapper/util/TestConstraintSnapshot.kt | 12 ++ 12 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/TimeUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 00b2fdc6af..c7ed7dc671 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,9 @@ android { } compileOptions { + // Required for desugaring new Java time API on lower than API 26 + coreLibraryDesugaringEnabled = true + sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } @@ -192,6 +195,7 @@ dependencies { proImplementation 'com.revenuecat.purchases:purchases:8.15.0' proImplementation "com.airbnb.android:lottie-compose:6.6.3" implementation("com.squareup.okhttp3:okhttp:4.12.0") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") // splitties implementation "com.louiscad.splitties:splitties-bitflags:$splitties_version" diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt index 6a0425827d..6ed94f2c43 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintScreen.kt @@ -62,6 +62,8 @@ fun ChooseConstraintScreen( val listItems by viewModel.listItems.collectAsStateWithLifecycle() val query by viewModel.searchQuery.collectAsStateWithLifecycle() + TimeConstraintBottomSheet(viewModel) + ChooseConstraintScreen( modifier = modifier, state = listItems, diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt index e1d066d326..ef87621a6e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ChooseConstraintViewModel.kt @@ -1,5 +1,8 @@ package io.github.sds100.keymapper.constraints +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -88,6 +91,8 @@ class ChooseConstraintViewModel( ConstraintId.CHARGING, ConstraintId.DISCHARGING, + + ConstraintId.TIME, ) } @@ -104,12 +109,14 @@ class ChooseConstraintViewModel( State.Data(filteredItems) }.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) - var timeConstraintState: Constraint.Time? = null + var timeConstraintState: Constraint.Time? by mutableStateOf(null) fun onDoneConfigTimeConstraintClick() { timeConstraintState?.let { constraint -> - _returnResult.tryEmit(constraint) - timeConstraintState = null + viewModelScope.launch { + _returnResult.emit(constraint) + timeConstraintState = null + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt index 9633d38cf6..ccb07858b0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/Constraint.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.util.getKey import io.github.sds100.keymapper.util.valueOrNull import kotlinx.serialization.Serializable +import java.time.LocalTime import java.util.UUID /** @@ -226,6 +227,9 @@ sealed class Constraint { val endMinute: Int, ) : Constraint() { override val id: ConstraintId = ConstraintId.TIME + + val startTime: LocalTime by lazy { LocalTime.of(startHour, startMinute) } + val endTime: LocalTime by lazy { LocalTime.of(endHour, endMinute) } } } @@ -639,5 +643,18 @@ object ConstraintEntityMapper { uid = constraint.uid, ConstraintEntity.DISCHARGING, ) + + is Constraint.Time -> ConstraintEntity( + uid = constraint.uid, + type = ConstraintEntity.TIME, + EntityExtra( + ConstraintEntity.EXTRA_START_TIME, + "${constraint.startHour}:${constraint.startMinute}", + ), + EntityExtra( + ConstraintEntity.EXTRA_END_TIME, + "${constraint.endHour}:${constraint.endMinute}", + ), + ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt index 4ef6ceb373..5a813bc92a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintSnapshot.kt @@ -17,6 +17,7 @@ import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.power.PowerAdapter import io.github.sds100.keymapper.util.firstBlocking import timber.log.Timber +import java.time.LocalTime /** * Created by sds100 on 08/05/2021.f @@ -69,6 +70,8 @@ class LazyConstraintSnapshot( lockScreenAdapter.isLockScreenShowing() } + private val localTime = LocalTime.now() + private fun isMediaPlaying(): Boolean { return audioVolumeStreams.contains(AudioManager.STREAM_MUSIC) || appsPlayingMedia.isNotEmpty() } @@ -156,6 +159,13 @@ class LazyConstraintSnapshot( // an another activity like the camera app while the phone is locked. is Constraint.LockScreenShowing -> isLockscreenShowing && appInForeground == "com.android.systemui" is Constraint.LockScreenNotShowing -> !isLockscreenShowing || appInForeground != "com.android.systemui" + + is Constraint.Time -> + if (constraint.startTime.isAfter(constraint.endTime)) { + localTime.isAfter(constraint.startTime) || localTime.isBefore(constraint.endTime) + } else { + localTime.isAfter(constraint.startTime) && localTime.isBefore(constraint.endTime) + } } if (isSatisfied) { diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUiHelper.kt index 7e2cc119d2..223b606c77 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUiHelper.kt @@ -5,10 +5,12 @@ import androidx.compose.material.icons.rounded.Android import io.github.sds100.keymapper.R import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation +import io.github.sds100.keymapper.util.TimeUtils import io.github.sds100.keymapper.util.handle import io.github.sds100.keymapper.util.ui.ResourceProvider import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.valueIfFailure +import java.time.format.FormatStyle /** * Created by sds100 on 18/03/2021. @@ -20,6 +22,8 @@ class ConstraintUiHelper( ) : DisplayConstraintUseCase by displayConstraintUseCase, ResourceProvider by resourceProvider { + private val timeFormatter by lazy { TimeUtils.localeDateFormatter(FormatStyle.SHORT) } + fun getTitle(constraint: Constraint): String = when (constraint) { is Constraint.AppInForeground -> getAppName(constraint.packageName).handle( @@ -144,6 +148,13 @@ class ConstraintUiHelper( is Constraint.Discharging -> getString(R.string.constraint_discharging) is Constraint.LockScreenShowing -> getString(R.string.constraint_lock_screen_showing) is Constraint.LockScreenNotShowing -> getString(R.string.constraint_lock_screen_not_showing) + is Constraint.Time -> getString( + R.string.constraint_time_formatted, + arrayOf( + timeFormatter.format(constraint.startTime), + timeFormatter.format(constraint.endTime), + ), + ) } fun getIcon(constraint: Constraint): ComposeIconInfo = when (constraint) { diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUtils.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUtils.kt index 94e5a4dd5e..db5e3421e9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintUtils.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.outlined.SignalWifiStatusbarNull import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait import androidx.compose.material.icons.outlined.StopCircle +import androidx.compose.material.icons.outlined.Timer import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material.icons.outlined.WifiOff import androidx.compose.material.icons.rounded.Android @@ -78,6 +79,7 @@ object ConstraintUtils { ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar) ConstraintId.LOCK_SCREEN_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.ScreenLockPortrait) ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) + ConstraintId.TIME -> ComposeIconInfo.Vector(Icons.Outlined.Timer) } fun getTitleStringId(constraintId: ConstraintId): Int = when (constraintId) { @@ -114,5 +116,6 @@ object ConstraintUtils { ConstraintId.DISCHARGING -> R.string.constraint_discharging ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing + ConstraintId.TIME -> R.string.constraint_time } } diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt index ce2e92d6d8..f59740fddd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/TimeConstraintBottomSheet.kt @@ -9,17 +9,31 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.rounded.Timer +import androidx.compose.material.icons.rounded.TimerOff +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerState import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -29,8 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.TimeUtils import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow import kotlinx.coroutines.launch +import java.time.format.FormatStyle @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -78,6 +94,39 @@ private fun TimeConstraintBottomSheet( onDoneClick: () -> Unit = {}, ) { val scope = rememberCoroutineScope() + val formatter = remember { TimeUtils.localeDateFormatter(FormatStyle.SHORT) } + + val startTimePickerState = rememberTimePickerState() + var showStartTimePickerDialog by remember { mutableStateOf(false) } + + if (showStartTimePickerDialog) { + TimePickerDialog( + state = startTimePickerState, + onDismiss = { + showStartTimePickerDialog = false + }, + onConfirm = { + onSelectStartTime(startTimePickerState.hour, startTimePickerState.minute) + showStartTimePickerDialog = false + }, + ) + } + + val endTimePickerState = rememberTimePickerState() + var showEndTimePickerDialog by remember { mutableStateOf(false) } + + if (showEndTimePickerDialog) { + TimePickerDialog( + state = endTimePickerState, + onDismiss = { + showEndTimePickerDialog = false + }, + onConfirm = { + onSelectEndTime(endTimePickerState.hour, endTimePickerState.minute) + showEndTimePickerDialog = false + }, + ) + } ModalBottomSheet( onDismissRequest = onDismissRequest, @@ -99,10 +148,66 @@ private fun TimeConstraintBottomSheet( Spacer(modifier = Modifier.height(8.dp)) OptionsHeaderRow( - icon = Icons.Rounded.Clo + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Timer, + text = stringResource(R.string.constraint_time_bottom_sheet_start_time), ) - Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = formatter.format(state.startTime), + style = MaterialTheme.typography.titleLarge, + ) + + IconButton( + modifier = Modifier.padding(start = 8.dp), + onClick = { + startTimePickerState.hour = state.startHour + startTimePickerState.minute = state.startMinute + + showStartTimePickerDialog = true + }, + ) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = stringResource(R.string.constraint_time_bottom_sheet_edit_start_time), + ) + } + } + + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.TimerOff, + text = stringResource(R.string.constraint_time_bottom_sheet_end_time), + ) + + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = formatter.format(state.endTime), + style = MaterialTheme.typography.titleLarge, + ) + + IconButton( + modifier = Modifier.padding(start = 8.dp), + onClick = { + endTimePickerState.hour = state.endHour + endTimePickerState.minute = state.endMinute + + showEndTimePickerDialog = true + }, + ) { + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = stringResource(R.string.constraint_time_bottom_sheet_edit_end_time), + ) + } + } Row( modifier = Modifier @@ -138,6 +243,31 @@ private fun TimeConstraintBottomSheet( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TimePickerDialog( + state: TimePickerState, + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(stringResource(R.string.neg_cancel)) + } + }, + confirmButton = { + TextButton(onClick = { onConfirm() }) { + Text(stringResource(R.string.pos_ok)) + } + }, + text = { + TimePicker(state = state) + }, + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt index 156a9a0061..7ba746f11e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/comparators/KeyMapConstraintsComparator.kt @@ -7,6 +7,8 @@ import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.then import io.github.sds100.keymapper.util.valueOrNull +import java.time.LocalDate +import java.time.ZoneOffset class KeyMapConstraintsComparator( private val displayConstraints: DisplayConstraintUseCase, @@ -125,6 +127,11 @@ class KeyMapConstraintsComparator( is Constraint.WifiOn -> Success("") is Constraint.LockScreenNotShowing -> Success("") is Constraint.LockScreenShowing -> Success("") + is Constraint.Time -> Success( + constraint.startTime + .toEpochSecond(LocalDate.now(), ZoneOffset.UTC) + .toString(), + ) } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/TimeUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/TimeUtils.kt new file mode 100644 index 0000000000..633e084da4 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/TimeUtils.kt @@ -0,0 +1,11 @@ +package io.github.sds100.keymapper.util + +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +object TimeUtils { + fun localeDateFormatter(style: FormatStyle): DateTimeFormatter { + return DateTimeFormatter.ofLocalizedTime(style).withLocale(Locale.getDefault()) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2aa020b759..b3bccda24a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -293,7 +293,9 @@ Time between %s and %s Time constraint Start time + Edit start time End time + Edit end time diff --git a/app/src/test/java/io/github/sds100/keymapper/util/TestConstraintSnapshot.kt b/app/src/test/java/io/github/sds100/keymapper/util/TestConstraintSnapshot.kt index ada802c501..6e2d46ec34 100644 --- a/app/src/test/java/io/github/sds100/keymapper/util/TestConstraintSnapshot.kt +++ b/app/src/test/java/io/github/sds100/keymapper/util/TestConstraintSnapshot.kt @@ -7,6 +7,7 @@ import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.phone.CallState import timber.log.Timber +import java.time.LocalTime class TestConstraintSnapshot( val appInForeground: String? = null, @@ -23,6 +24,7 @@ class TestConstraintSnapshot( val isBackFlashlightOn: Boolean = false, val isFrontFlashlightOn: Boolean = false, val isLockscreenShowing: Boolean = false, + val localTime: LocalTime = LocalTime.now(), ) : ConstraintSnapshot { override fun isSatisfied(constraint: Constraint): Boolean { @@ -94,6 +96,16 @@ class TestConstraintSnapshot( is Constraint.Discharging -> !isCharging is Constraint.LockScreenShowing -> isLockscreenShowing is Constraint.LockScreenNotShowing -> !isLockscreenShowing + is Constraint.Time -> { + val startTime = constraint.startTime + val endTime = constraint.endTime + + if (startTime.isAfter(endTime)) { + localTime.isAfter(startTime) || localTime.isBefore(endTime) + } else { + localTime.isAfter(startTime) && localTime.isBefore(endTime) + } + } } if (isSatisfied) { From 43bace57d5985a94d4f951415efe02a9a145603f Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 29 Apr 2025 12:47:06 +0200 Subject: [PATCH 05/69] chore: add time constraints to changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb8048a0a..3b43105a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) + +#### TO BE RELEASED + +## Added + +- #699 Time constraints ⏰ + ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 From fd1e9934cd9fb4aa1549bea0cf70e7436dd8c30e Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 29 Apr 2025 12:49:55 +0200 Subject: [PATCH 06/69] chore: bump version to 3.1.0 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index aa81264386..c6beedf368 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.0.1 -VERSION_CODE=103 +VERSION_NAME=3.1.0 +VERSION_CODE=104 VERSION_NUM=0 \ No newline at end of file From d05986480b2e51c708e7e30ed0a104486baf9172 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 30 Apr 2025 09:09:28 +0200 Subject: [PATCH 07/69] #257 WIP: create interact with UI element action --- .../sds100/keymapper/actions/ActionData.kt | 147 ++++++++------- .../actions/ActionDataEntityMapper.kt | 173 +++++++++++++++++- .../sds100/keymapper/actions/ActionId.kt | 1 + .../keymapper/actions/ActionUiHelper.kt | 2 + .../sds100/keymapper/actions/ActionUtils.kt | 6 + .../keymapper/data/entities/ActionEntity.kt | 17 ++ .../AccessibilityNodeRepository.kt | 1 + .../accessibility/AccessibilityNodeModel.kt | 15 ++ .../accessibility/AccessibilityUtils.kt | 9 + .../util/ui/compose/icons/JumpToElement.kt | 115 ++++++++++++ app/src/main/res/values/strings.xml | 41 ++++- 11 files changed, 450 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/JumpToElement.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 5f4799294d..38a2587ffc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType +import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.intents.IntentExtraModel @@ -260,7 +261,7 @@ sealed class ActionData : Comparable { } @Serializable - object Disable : DoNotDisturb() { + data object Disable : DoNotDisturb() { override val id = ActionId.DISABLE_DND_MODE } } @@ -268,32 +269,32 @@ sealed class ActionData : Comparable { @Serializable sealed class Rotation : ActionData() { @Serializable - object EnableAuto : Rotation() { + data object EnableAuto : Rotation() { override val id = ActionId.ENABLE_AUTO_ROTATE } @Serializable - object DisableAuto : Rotation() { + data object DisableAuto : Rotation() { override val id = ActionId.DISABLE_AUTO_ROTATE } @Serializable - object ToggleAuto : Rotation() { + data object ToggleAuto : Rotation() { override val id = ActionId.TOGGLE_AUTO_ROTATE } @Serializable - object Portrait : Rotation() { + data object Portrait : Rotation() { override val id = ActionId.PORTRAIT_MODE } @Serializable - object Landscape : Rotation() { + data object Landscape : Rotation() { override val id = ActionId.LANDSCAPE_MODE } @Serializable - object SwitchOrientation : Rotation() { + data object SwitchOrientation : Rotation() { override val id = ActionId.SWITCH_ORIENTATION } @@ -369,37 +370,37 @@ sealed class ActionData : Comparable { @Serializable sealed class ControlMedia : ActionData() { @Serializable - object Pause : ControlMedia() { + data object Pause : ControlMedia() { override val id = ActionId.PAUSE_MEDIA } @Serializable - object Play : ControlMedia() { + data object Play : ControlMedia() { override val id = ActionId.PLAY_MEDIA } @Serializable - object PlayPause : ControlMedia() { + data object PlayPause : ControlMedia() { override val id = ActionId.PLAY_PAUSE_MEDIA } @Serializable - object NextTrack : ControlMedia() { + data object NextTrack : ControlMedia() { override val id = ActionId.NEXT_TRACK } @Serializable - object PreviousTrack : ControlMedia() { + data object PreviousTrack : ControlMedia() { override val id = ActionId.PREVIOUS_TRACK } @Serializable - object FastForward : ControlMedia() { + data object FastForward : ControlMedia() { override val id = ActionId.FAST_FORWARD } @Serializable - object Rewind : ControlMedia() { + data object Rewind : ControlMedia() { override val id = ActionId.REWIND } } @@ -537,17 +538,17 @@ sealed class ActionData : Comparable { @Serializable sealed class Wifi : ActionData() { @Serializable - object Enable : Wifi() { + data object Enable : Wifi() { override val id = ActionId.ENABLE_WIFI } @Serializable - object Disable : Wifi() { + data object Disable : Wifi() { override val id = ActionId.DISABLE_WIFI } @Serializable - object Toggle : Wifi() { + data object Toggle : Wifi() { override val id = ActionId.TOGGLE_WIFI } } @@ -555,17 +556,17 @@ sealed class ActionData : Comparable { @Serializable sealed class Bluetooth : ActionData() { @Serializable - object Enable : Bluetooth() { + data object Enable : Bluetooth() { override val id = ActionId.ENABLE_BLUETOOTH } @Serializable - object Disable : Bluetooth() { + data object Disable : Bluetooth() { override val id = ActionId.DISABLE_BLUETOOTH } @Serializable - object Toggle : Bluetooth() { + data object Toggle : Bluetooth() { override val id = ActionId.TOGGLE_BLUETOOTH } } @@ -573,17 +574,17 @@ sealed class ActionData : Comparable { @Serializable sealed class Nfc : ActionData() { @Serializable - object Enable : Nfc() { + data object Enable : Nfc() { override val id = ActionId.ENABLE_NFC } @Serializable - object Disable : Nfc() { + data object Disable : Nfc() { override val id = ActionId.DISABLE_NFC } @Serializable - object Toggle : Nfc() { + data object Toggle : Nfc() { override val id = ActionId.TOGGLE_NFC } } @@ -591,17 +592,17 @@ sealed class ActionData : Comparable { @Serializable sealed class AirplaneMode : ActionData() { @Serializable - object Enable : AirplaneMode() { + data object Enable : AirplaneMode() { override val id = ActionId.ENABLE_AIRPLANE_MODE } @Serializable - object Disable : AirplaneMode() { + data object Disable : AirplaneMode() { override val id = ActionId.DISABLE_AIRPLANE_MODE } @Serializable - object Toggle : AirplaneMode() { + data object Toggle : AirplaneMode() { override val id = ActionId.TOGGLE_AIRPLANE_MODE } } @@ -609,17 +610,17 @@ sealed class ActionData : Comparable { @Serializable sealed class MobileData : ActionData() { @Serializable - object Enable : MobileData() { + data object Enable : MobileData() { override val id = ActionId.ENABLE_MOBILE_DATA } @Serializable - object Disable : MobileData() { + data object Disable : MobileData() { override val id = ActionId.DISABLE_MOBILE_DATA } @Serializable - object Toggle : MobileData() { + data object Toggle : MobileData() { override val id = ActionId.TOGGLE_MOBILE_DATA } } @@ -627,27 +628,27 @@ sealed class ActionData : Comparable { @Serializable sealed class Brightness : ActionData() { @Serializable - object EnableAuto : Brightness() { + data object EnableAuto : Brightness() { override val id = ActionId.ENABLE_AUTO_BRIGHTNESS } @Serializable - object DisableAuto : Brightness() { + data object DisableAuto : Brightness() { override val id = ActionId.DISABLE_AUTO_BRIGHTNESS } @Serializable - object ToggleAuto : Brightness() { + data object ToggleAuto : Brightness() { override val id = ActionId.TOGGLE_AUTO_BRIGHTNESS } @Serializable - object Increase : Brightness() { + data object Increase : Brightness() { override val id = ActionId.INCREASE_BRIGHTNESS } @Serializable - object Decrease : Brightness() { + data object Decrease : Brightness() { override val id = ActionId.DECREASE_BRIGHTNESS } } @@ -655,178 +656,178 @@ sealed class ActionData : Comparable { @Serializable sealed class StatusBar : ActionData() { @Serializable - object ExpandNotifications : StatusBar() { + data object ExpandNotifications : StatusBar() { override val id = ActionId.EXPAND_NOTIFICATION_DRAWER } @Serializable - object ToggleNotifications : StatusBar() { + data object ToggleNotifications : StatusBar() { override val id = ActionId.TOGGLE_NOTIFICATION_DRAWER } @Serializable - object ExpandQuickSettings : StatusBar() { + data object ExpandQuickSettings : StatusBar() { override val id = ActionId.EXPAND_QUICK_SETTINGS } @Serializable - object ToggleQuickSettings : StatusBar() { + data object ToggleQuickSettings : StatusBar() { override val id = ActionId.TOGGLE_QUICK_SETTINGS } @Serializable - object Collapse : StatusBar() { + data object Collapse : StatusBar() { override val id = ActionId.COLLAPSE_STATUS_BAR } } @Serializable - object GoBack : ActionData() { + data object GoBack : ActionData() { override val id = ActionId.GO_BACK } @Serializable - object GoHome : ActionData() { + data object GoHome : ActionData() { override val id = ActionId.GO_HOME } @Serializable - object OpenRecents : ActionData() { + data object OpenRecents : ActionData() { override val id = ActionId.OPEN_RECENTS } @Serializable - object GoLastApp : ActionData() { + data object GoLastApp : ActionData() { override val id = ActionId.GO_LAST_APP } @Serializable - object OpenMenu : ActionData() { + data object OpenMenu : ActionData() { override val id = ActionId.OPEN_MENU } @Serializable - object ToggleSplitScreen : ActionData() { + data object ToggleSplitScreen : ActionData() { override val id = ActionId.TOGGLE_SPLIT_SCREEN } @Serializable - object Screenshot : ActionData() { + data object Screenshot : ActionData() { override val id = ActionId.SCREENSHOT } @Serializable - object MoveCursorToEnd : ActionData() { + data object MoveCursorToEnd : ActionData() { override val id = ActionId.MOVE_CURSOR_TO_END } @Serializable - object ToggleKeyboard : ActionData() { + data object ToggleKeyboard : ActionData() { override val id = ActionId.TOGGLE_KEYBOARD } @Serializable - object ShowKeyboard : ActionData() { + data object ShowKeyboard : ActionData() { override val id = ActionId.SHOW_KEYBOARD } @Serializable - object HideKeyboard : ActionData() { + data object HideKeyboard : ActionData() { override val id = ActionId.HIDE_KEYBOARD } @Serializable - object ShowKeyboardPicker : ActionData() { + data object ShowKeyboardPicker : ActionData() { override val id = ActionId.SHOW_KEYBOARD_PICKER } @Serializable - object CopyText : ActionData() { + data object CopyText : ActionData() { override val id = ActionId.TEXT_COPY } @Serializable - object PasteText : ActionData() { + data object PasteText : ActionData() { override val id = ActionId.TEXT_PASTE } @Serializable - object CutText : ActionData() { + data object CutText : ActionData() { override val id = ActionId.TEXT_CUT } @Serializable - object SelectWordAtCursor : ActionData() { + data object SelectWordAtCursor : ActionData() { override val id = ActionId.SELECT_WORD_AT_CURSOR } @Serializable - object VoiceAssistant : ActionData() { + data object VoiceAssistant : ActionData() { override val id = ActionId.OPEN_VOICE_ASSISTANT } @Serializable - object DeviceAssistant : ActionData() { + data object DeviceAssistant : ActionData() { override val id = ActionId.OPEN_DEVICE_ASSISTANT } @Serializable - object OpenCamera : ActionData() { + data object OpenCamera : ActionData() { override val id = ActionId.OPEN_CAMERA } @Serializable - object LockDevice : ActionData() { + data object LockDevice : ActionData() { override val id = ActionId.LOCK_DEVICE } @Serializable - object ScreenOnOff : ActionData() { + data object ScreenOnOff : ActionData() { override val id = ActionId.POWER_ON_OFF_DEVICE } @Serializable - object SecureLock : ActionData() { + data object SecureLock : ActionData() { override val id = ActionId.SECURE_LOCK_DEVICE } @Serializable - object ConsumeKeyEvent : ActionData() { + data object ConsumeKeyEvent : ActionData() { override val id = ActionId.CONSUME_KEY_EVENT } @Serializable - object OpenSettings : ActionData() { + data object OpenSettings : ActionData() { override val id = ActionId.OPEN_SETTINGS } @Serializable - object ShowPowerMenu : ActionData() { + data object ShowPowerMenu : ActionData() { override val id = ActionId.SHOW_POWER_MENU } @Serializable - object DismissLastNotification : ActionData() { + data object DismissLastNotification : ActionData() { override val id: ActionId = ActionId.DISMISS_MOST_RECENT_NOTIFICATION } @Serializable - object DismissAllNotifications : ActionData() { + data object DismissAllNotifications : ActionData() { override val id: ActionId = ActionId.DISMISS_ALL_NOTIFICATIONS } @Serializable - object AnswerCall : ActionData() { + data object AnswerCall : ActionData() { override val id: ActionId = ActionId.ANSWER_PHONE_CALL } @Serializable - object EndCall : ActionData() { + data object EndCall : ActionData() { override val id: ActionId = ActionId.END_PHONE_CALL } @Serializable - object DeviceControls : ActionData() { + data object DeviceControls : ActionData() { override val id: ActionId = ActionId.DEVICE_CONTROLS } @@ -845,4 +846,12 @@ sealed class ActionData : Comparable { return "HttpRequest(description=$description)" } } + + @Serializable + data class InteractUiElement( + val nodeAction: Int, + val node: AccessibilityNodeModel, + ) : ActionData() { + override val id: ActionId = ActionId.INTERACT_UI_ELEMENT + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 53a6515254..063dd5d8c2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.data.db.typeconverter.ConstantTypeConverters import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.data.entities.EntityExtra import io.github.sds100.keymapper.data.entities.getData +import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.intents.IntentExtraModel import io.github.sds100.keymapper.system.intents.IntentTarget @@ -12,6 +13,7 @@ import io.github.sds100.keymapper.system.network.HttpMethod import io.github.sds100.keymapper.system.volume.DndMode import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeStream +import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.getKey import io.github.sds100.keymapper.util.success import io.github.sds100.keymapper.util.then @@ -41,6 +43,8 @@ object ActionDataEntityMapper { ActionEntity.Type.SYSTEM_ACTION -> { SYSTEM_ACTION_ID_MAP.getKey(entity.data) ?: return null } + + ActionEntity.Type.INTERACT_UI_ELEMENT -> ActionId.INTERACT_UI_ELEMENT } return when (actionId) { @@ -266,7 +270,7 @@ object ActionDataEntityMapper { ActionId.VOLUME_TOGGLE_MUTE, ActionId.VOLUME_UNMUTE, ActionId.VOLUME_MUTE, - -> { + -> { val showVolumeUi = entity.flags.hasFlag(ActionEntity.ACTION_FLAG_SHOW_VOLUME_UI) @@ -287,7 +291,7 @@ object ActionDataEntityMapper { ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> { + -> { val lens = entity.extras.getData(ActionEntity.EXTRA_LENS).then { LENS_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null @@ -313,7 +317,7 @@ object ActionDataEntityMapper { } ActionId.DISABLE_FLASHLIGHT, - -> { + -> { val lens = entity.extras.getData(ActionEntity.EXTRA_LENS).then { LENS_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null @@ -322,7 +326,7 @@ object ActionDataEntityMapper { ActionId.TOGGLE_DND_MODE, ActionId.ENABLE_DND_MODE, - -> { + -> { val dndMode = entity.extras.getData(ActionEntity.EXTRA_DND_MODE).then { DND_MODE_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null @@ -349,7 +353,7 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, - -> { + -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_PACKAGE_NAME).valueOrNull() ?: return null @@ -522,6 +526,70 @@ object ActionDataEntityMapper { authorizationHeader = authorizationHeader, ) } + + ActionId.INTERACT_UI_ELEMENT -> { + val packageName = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME) + .valueOrNull() + + val contentDescription = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION) + .valueOrNull() + + val isFocused = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_IS_FOCUSED) + .then { Success(it.toBoolean()) }.valueOrNull() ?: false + + val text = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT).valueOrNull() + + val textSelectionStart = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_START) + .then { Success(it.toInt()) }.valueOrNull() ?: 0 + + val textSelectionEnd = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_END) + .then { Success(it.toInt()) }.valueOrNull() ?: 0 + + val isEditable = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_IS_EDITABLE) + .then { Success(it.toBoolean()) }.valueOrNull() ?: false + + val className = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME).valueOrNull() + + val viewResourceId = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID) + .valueOrNull() + + val uniqueId = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID).valueOrNull() + + val actions = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS).then { + val intList = it.split(",").map { action -> action.toInt() } + Success(intList) + }.valueOrNull() ?: emptyList() + + val nodeAction = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_NODE_ACTION).then { + Success(it.toInt()) + }.valueOrNull()!! + + ActionData.InteractUiElement( + nodeAction = nodeAction, + node = AccessibilityNodeModel( + packageName = packageName, + contentDescription = contentDescription, + isFocused = isFocused, + text = text, + textSelectionStart = textSelectionStart, + textSelectionEnd = textSelectionEnd, + isEditable = isEditable, + className = className, + uniqueId = uniqueId, + viewResourceId = viewResourceId, + actions = actions, + ), + ) + } } } @@ -538,6 +606,7 @@ object ActionDataEntityMapper { is ActionData.Text -> ActionEntity.Type.TEXT_BLOCK is ActionData.Url -> ActionEntity.Type.URL is ActionData.Sound -> ActionEntity.Type.SOUND + is ActionData.InteractUiElement -> ActionEntity.Type.INTERACT_UI_ELEMENT else -> ActionEntity.Type.SYSTEM_ACTION } @@ -579,6 +648,7 @@ object ActionDataEntityMapper { is ActionData.Text -> data.text is ActionData.Url -> data.url is ActionData.Sound -> data.soundUid + is ActionData.InteractUiElement -> "" // No data string needed for UI element interaction else -> SYSTEM_ACTION_ID_MAP[data.id]!! } @@ -750,6 +820,99 @@ object ActionDataEntityMapper { ), ) + is ActionData.InteractUiElement -> buildList { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_NODE_ACTION, + data.nodeAction.toString(), + ), + ) + + data.node.packageName?.let { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME, + it, + ), + ) + } + + data.node.contentDescription?.let { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION, + it, + ), + ) + } + + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_IS_FOCUSED, + data.node.isFocused.toString(), + ), + ) + + data.node.text?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TEXT, it)) } + + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_START, + data.node.textSelectionStart.toString(), + ), + ) + + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_END, + data.node.textSelectionEnd.toString(), + ), + ) + + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_IS_EDITABLE, + data.node.isEditable.toString(), + ), + ) + + data.node.className?.let { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME, + it, + ), + ) + } + + data.node.viewResourceId?.let { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID, + it, + ), + ) + } + + data.node.uniqueId?.let { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID, + it, + ), + ) + } + + if (data.node.actions.isNotEmpty()) { + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS, + data.node.actions.joinToString(","), + ), + ) + } + } + else -> emptyList() } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt index 515474d744..011c7f825a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt @@ -16,6 +16,7 @@ enum class ActionId { INTENT, PHONE_CALL, SOUND, + INTERACT_UI_ELEMENT, TOGGLE_WIFI, ENABLE_WIFI, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index 755353b04d..be4ebb39e4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -529,6 +529,8 @@ class ActionUiHelper( ActionData.DeviceControls -> getString(R.string.action_device_controls) is ActionData.HttpRequest -> action.description + + is ActionData.InteractUiElement -> getString(R.string.action_interact_ui_element_title) } fun getIcon(action: ActionData): ComposeIconInfo = when (action) { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index e3b696fc8c..12b23dd67f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -73,6 +73,7 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.ui.compose.icons.HomeIotDevice import io.github.sds100.keymapper.util.ui.compose.icons.InstantMix +import io.github.sds100.keymapper.util.ui.compose.icons.JumpToElement import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons import io.github.sds100.keymapper.util.ui.compose.icons.MatchWord import io.github.sds100.keymapper.util.ui.compose.icons.NfcOff @@ -230,6 +231,8 @@ object ActionUtils { ActionId.DISMISS_MOST_RECENT_NOTIFICATION -> ActionCategory.NOTIFICATIONS ActionId.DISMISS_ALL_NOTIFICATIONS -> ActionCategory.NOTIFICATIONS ActionId.DEVICE_CONTROLS -> ActionCategory.APPS + + ActionId.INTERACT_UI_ELEMENT -> ActionCategory.APPS } @StringRes @@ -342,6 +345,7 @@ object ActionUtils { ActionId.END_PHONE_CALL -> R.string.action_end_call ActionId.DEVICE_CONTROLS -> R.string.action_device_controls ActionId.HTTP_REQUEST -> R.string.action_http_request + ActionId.INTERACT_UI_ELEMENT -> R.string.action_interact_ui_element_title } @DrawableRes @@ -454,6 +458,7 @@ object ActionUtils { ActionId.END_PHONE_CALL -> R.drawable.ic_outline_call_end_24 ActionId.DEVICE_CONTROLS -> R.drawable.ic_home_automation ActionId.HTTP_REQUEST -> null + ActionId.INTERACT_UI_ELEMENT -> null } fun getMinApi(id: ActionId): Int = when (id) { @@ -770,6 +775,7 @@ object ActionUtils { ActionId.END_PHONE_CALL -> Icons.Outlined.CallEnd ActionId.DEVICE_CONTROLS -> KeyMapperIcons.HomeIotDevice ActionId.HTTP_REQUEST -> Icons.Outlined.Http + ActionId.INTERACT_UI_ELEMENT -> KeyMapperIcons.JumpToElement } } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index e99db4de73..2160feaf75 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -90,6 +90,22 @@ data class ActionEntity( const val EXTRA_HTTP_DESCRIPTION = "extra_http_description" const val EXTRA_HTTP_AUTHORIZATION_HEADER = "extra_http_authorization_header" + // Accessibility node extras + const val EXTRA_ACCESSIBILITY_PACKAGE_NAME = "extra_accessibility_package_name" + const val EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION = + "extra_accessibility_content_description" + const val EXTRA_ACCESSIBILITY_IS_FOCUSED = "extra_accessibility_is_focused" + const val EXTRA_ACCESSIBILITY_TEXT = "extra_accessibility_text" + const val EXTRA_ACCESSIBILITY_TEXT_SELECTION_START = + "extra_accessibility_text_selection_start" + const val EXTRA_ACCESSIBILITY_TEXT_SELECTION_END = "extra_accessibility_text_selection_end" + const val EXTRA_ACCESSIBILITY_IS_EDITABLE = "extra_accessibility_is_editable" + const val EXTRA_ACCESSIBILITY_CLASS_NAME = "extra_accessibility_class_name" + const val EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID = "extra_accessibility_view_resource_id" + const val EXTRA_ACCESSIBILITY_UNIQUE_ID = "extra_accessibility_unique_id" + const val EXTRA_ACCESSIBILITY_ACTIONS = "extra_accessibility_actions" + const val EXTRA_ACCESSIBILITY_NODE_ACTION = "extra_accessibility_node_action" + // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_ACTION_TYPE = "type" const val NAME_DATA = "data" @@ -153,6 +169,7 @@ data class ActionEntity( INTENT, PHONE_CALL, SOUND, + INTERACT_UI_ELEMENT, } constructor( diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt index b2fa1e522f..be21fa91a6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt @@ -14,6 +14,7 @@ interface AccessibilityNodeRepository { } class AccessibilityNodeRepositoryImpl(private val coroutineScope: CoroutineScope) : AccessibilityNodeRepository { + // TODO have a DAO to remember between app launches and so it isn't all cached in memory? override val nodes = MutableStateFlow>>(State.Data(ArrayList(128))) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeModel.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeModel.kt index 934c0ace2f..08f5a14e04 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeModel.kt @@ -1,8 +1,13 @@ package io.github.sds100.keymapper.system.accessibility +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.serialization.Serializable + /** * Created by sds100 on 21/04/2021. */ +@Serializable data class AccessibilityNodeModel( val packageName: String?, val contentDescription: String?, @@ -11,4 +16,14 @@ data class AccessibilityNodeModel( val textSelectionStart: Int, val textSelectionEnd: Int, val isEditable: Boolean, + val className: String?, + val viewResourceId: String?, + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + val uniqueId: String?, + + /** + * A list of the allowed accessibility node actions. + */ + val actions: List, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt index 1f16e4da93..7fff55c865 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.system.accessibility +import android.os.Build import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -38,6 +39,14 @@ fun AccessibilityNodeInfo.toModel(): AccessibilityNodeModel = AccessibilityNodeM textSelectionEnd = textSelectionEnd, text = text?.toString(), isEditable = isEditable, + className = className?.toString(), + uniqueId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + uniqueId + } else { + null + }, + viewResourceId = viewIdResourceName, + actions = actionList.map { it.id }, ) fun AccessibilityEvent.toModel(): AccessibilityEventModel = AccessibilityEventModel(eventTime, eventType) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/JumpToElement.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/JumpToElement.kt new file mode 100644 index 0000000000..498d3bc295 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/JumpToElement.kt @@ -0,0 +1,115 @@ +package io.github.sds100.keymapper.util.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.JumpToElement: ImageVector + get() { + if (_JumpToElement != null) { + return _JumpToElement!! + } + _JumpToElement = ImageVector.Builder( + name = "JumpToElement", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path(fill = SolidColor(Color(0xFF000000))) { + moveTo(520f, 440f) + lineTo(560f, 440f) + quadTo(577f, 440f, 588.5f, 451.5f) + quadTo(600f, 463f, 600f, 480f) + quadTo(600f, 497f, 588.5f, 508.5f) + quadTo(577f, 520f, 560f, 520f) + lineTo(480f, 520f) + quadTo(463f, 520f, 451.5f, 508.5f) + quadTo(440f, 497f, 440f, 480f) + lineTo(440f, 400f) + quadTo(440f, 383f, 451.5f, 371.5f) + quadTo(463f, 360f, 480f, 360f) + quadTo(497f, 360f, 508.5f, 371.5f) + quadTo(520f, 383f, 520f, 400f) + lineTo(520f, 440f) + close() + moveTo(800f, 440f) + lineTo(800f, 400f) + quadTo(800f, 383f, 811.5f, 371.5f) + quadTo(823f, 360f, 840f, 360f) + quadTo(857f, 360f, 868.5f, 371.5f) + quadTo(880f, 383f, 880f, 400f) + lineTo(880f, 480f) + quadTo(880f, 497f, 868.5f, 508.5f) + quadTo(857f, 520f, 840f, 520f) + lineTo(760f, 520f) + quadTo(743f, 520f, 731.5f, 508.5f) + quadTo(720f, 497f, 720f, 480f) + quadTo(720f, 463f, 731.5f, 451.5f) + quadTo(743f, 440f, 760f, 440f) + lineTo(800f, 440f) + close() + moveTo(520f, 160f) + lineTo(520f, 200f) + quadTo(520f, 217f, 508.5f, 228.5f) + quadTo(497f, 240f, 480f, 240f) + quadTo(463f, 240f, 451.5f, 228.5f) + quadTo(440f, 217f, 440f, 200f) + lineTo(440f, 120f) + quadTo(440f, 103f, 451.5f, 91.5f) + quadTo(463f, 80f, 480f, 80f) + lineTo(560f, 80f) + quadTo(577f, 80f, 588.5f, 91.5f) + quadTo(600f, 103f, 600f, 120f) + quadTo(600f, 137f, 588.5f, 148.5f) + quadTo(577f, 160f, 560f, 160f) + lineTo(520f, 160f) + close() + moveTo(800f, 160f) + lineTo(760f, 160f) + quadTo(743f, 160f, 731.5f, 148.5f) + quadTo(720f, 137f, 720f, 120f) + quadTo(720f, 103f, 731.5f, 91.5f) + quadTo(743f, 80f, 760f, 80f) + lineTo(840f, 80f) + quadTo(857f, 80f, 868.5f, 91.5f) + quadTo(880f, 103f, 880f, 120f) + lineTo(880f, 200f) + quadTo(880f, 217f, 868.5f, 228.5f) + quadTo(857f, 240f, 840f, 240f) + quadTo(823f, 240f, 811.5f, 228.5f) + quadTo(800f, 217f, 800f, 200f) + lineTo(800f, 160f) + close() + moveTo(360f, 656f) + lineTo(164f, 852f) + quadTo(153f, 863f, 136f, 863f) + quadTo(119f, 863f, 108f, 852f) + quadTo(97f, 841f, 97f, 824f) + quadTo(97f, 807f, 108f, 796f) + lineTo(304f, 600f) + lineTo(160f, 600f) + quadTo(143f, 600f, 131.5f, 588.5f) + quadTo(120f, 577f, 120f, 560f) + quadTo(120f, 543f, 131.5f, 531.5f) + quadTo(143f, 520f, 160f, 520f) + lineTo(400f, 520f) + quadTo(417f, 520f, 428.5f, 531.5f) + quadTo(440f, 543f, 440f, 560f) + lineTo(440f, 800f) + quadTo(440f, 817f, 428.5f, 828.5f) + quadTo(417f, 840f, 400f, 840f) + quadTo(383f, 840f, 371.5f, 828.5f) + quadTo(360f, 817f, 360f, 800f) + lineTo(360f, 656f) + close() + } + }.build() + + return _JumpToElement!! + } + +@Suppress("ObjectPropertyName") +private var _JumpToElement: ImageVector? = null diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46985421d3..7d4e31a44c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1048,6 +1048,7 @@ This action will only work if you have tapped on an input field where the keyboard is supposed to be shown. Show keyboard Hide keyboard + Show keyboard picker Switch keyboard @@ -1061,9 +1062,9 @@ Open settings Show power menu - Toggle Airplane mode - Enable Airplane mode - Disable Airplane mode + Toggle airplane mode + Enable airplane mode + Disable airplane mode Launch app Some devices require apps to have permission before they can launch apps in the background. Tap \"Read more\" to view the instructions on our website. @@ -1098,6 +1099,40 @@ Request body (optional) Authorization header (optional) You must prepend \'Bearer\' if necessary + + Interact with app element + Key Mapper can detect and interact with app elements like menus, tabs, buttons and checkboxes. You need to record yourself interacting with the app element so that Key Mapper knows what you want to do. + Start recording + Stop recording (%s min left) + Go to another app and interact with it. Key Mapper will record what you do and you can choose which interactions you want to use in your key map. Open Key Mapper again when you’re done. + + %d interaction detected + %d interactions detected + + Choose the app to interact with + Record again + Choose app element + Choose the element you want your key map to interact with. + Can\'t find what you’re looking for? + Not all apps are compatible. For incompatible apps you can try the Tap Screen action instead. + Interaction type + Select how you want to interact with the UI element. + Filter interaction type + + Tap + Tap and hold + Focus + Select + Scroll forward + Scroll backward + Expand + Collapse + + Text + + View ID + Class name + Unique ID From f1e1ec5c198b0d272a6791171ba2d03542f41b80 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 1 May 2025 17:30:35 +0200 Subject: [PATCH 08/69] #257 WIP: create interact with UI element screen --- .../sds100/keymapper/actions/ActionData.kt | 13 +- .../actions/ActionDataEntityMapper.kt | 84 ++------- .../keymapper/actions/CreateActionDelegate.kt | 13 ++ .../actions/PerformActionsUseCase.kt | 15 ++ .../uielement/InteractUiElementFragment.kt | 80 +++++++++ .../uielement/InteractUiElementScreen.kt | 169 ++++++++++++++++++ .../uielement/InteractUiElementUseCase.kt | 73 ++++++++ .../uielement/InteractUiElementViewModel.kt | 160 +++++++++++++++++ .../data/entities/AccessibilityNodeEntity.kt | 8 +- .../keymapper/data/entities/ActionEntity.kt | 5 - .../accessibility/AccessibilityEventModel.kt | 6 - .../AccessibilityNodeRecorder.kt | 67 ++++--- .../accessibility/AccessibilityUtils.kt | 3 - .../BaseAccessibilityServiceController.kt | 20 ++- .../accessibility/MyAccessibilityService.kt | 1 + .../RecordAccessibilityNodeState.kt | 15 ++ .../io/github/sds100/keymapper/util/Inject.kt | 14 ++ .../sds100/keymapper/util/ServiceEvent.kt | 4 + .../keymapper/util/ui/NavDestination.kt | 5 + .../keymapper/util/ui/NavigationViewModel.kt | 6 +- app/src/main/res/navigation/nav_app.xml | 23 +++ app/src/main/res/values/strings.xml | 1 + 22 files changed, 673 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt delete mode 100644 app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityEventModel.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/system/accessibility/RecordAccessibilityNodeState.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 38a2587ffc..f2d4e8faa1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType -import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.intents.IntentExtraModel @@ -849,8 +848,18 @@ sealed class ActionData : Comparable { @Serializable data class InteractUiElement( + val description: String, val nodeAction: Int, - val node: AccessibilityNodeModel, + val packageName: String, + val text: String?, + val contentDescription: String?, + val className: String?, + val viewResourceId: String?, + val uniqueId: String?, + /** + * A list of the allowed accessibility node actions. + */ + val nodeActions: List, ) : ActionData() { override val id: ActionId = ActionId.INTERACT_UI_ELEMENT } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 063dd5d8c2..bcb0d4f8fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -5,7 +5,6 @@ import io.github.sds100.keymapper.data.db.typeconverter.ConstantTypeConverters import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.data.entities.EntityExtra import io.github.sds100.keymapper.data.entities.getData -import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.intents.IntentExtraModel import io.github.sds100.keymapper.system.intents.IntentTarget @@ -530,29 +529,15 @@ object ActionDataEntityMapper { ActionId.INTERACT_UI_ELEMENT -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME) - .valueOrNull() + .valueOrNull()!! val contentDescription = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION) .valueOrNull() - val isFocused = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_IS_FOCUSED) - .then { Success(it.toBoolean()) }.valueOrNull() ?: false - val text = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT).valueOrNull() - val textSelectionStart = - entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_START) - .then { Success(it.toInt()) }.valueOrNull() ?: 0 - - val textSelectionEnd = - entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_END) - .then { Success(it.toInt()) }.valueOrNull() ?: 0 - - val isEditable = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_IS_EDITABLE) - .then { Success(it.toBoolean()) }.valueOrNull() ?: false - val className = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME).valueOrNull() @@ -574,20 +559,15 @@ object ActionDataEntityMapper { }.valueOrNull()!! ActionData.InteractUiElement( + description = entity.data, nodeAction = nodeAction, - node = AccessibilityNodeModel( - packageName = packageName, - contentDescription = contentDescription, - isFocused = isFocused, - text = text, - textSelectionStart = textSelectionStart, - textSelectionEnd = textSelectionEnd, - isEditable = isEditable, - className = className, - uniqueId = uniqueId, - viewResourceId = viewResourceId, - actions = actions, - ), + packageName = packageName, + text = text, + contentDescription = contentDescription, + className = className, + viewResourceId = viewResourceId, + uniqueId = uniqueId, + nodeActions = actions, ) } } @@ -648,7 +628,7 @@ object ActionDataEntityMapper { is ActionData.Text -> data.text is ActionData.Url -> data.url is ActionData.Sound -> data.soundUid - is ActionData.InteractUiElement -> "" // No data string needed for UI element interaction + is ActionData.InteractUiElement -> data.description else -> SYSTEM_ACTION_ID_MAP[data.id]!! } @@ -828,7 +808,7 @@ object ActionDataEntityMapper { ), ) - data.node.packageName?.let { + data.packageName.let { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME, @@ -837,7 +817,7 @@ object ActionDataEntityMapper { ) } - data.node.contentDescription?.let { + data.contentDescription?.let { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION, @@ -846,37 +826,9 @@ object ActionDataEntityMapper { ) } - add( - EntityExtra( - ActionEntity.EXTRA_ACCESSIBILITY_IS_FOCUSED, - data.node.isFocused.toString(), - ), - ) - - data.node.text?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TEXT, it)) } - - add( - EntityExtra( - ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_START, - data.node.textSelectionStart.toString(), - ), - ) - - add( - EntityExtra( - ActionEntity.EXTRA_ACCESSIBILITY_TEXT_SELECTION_END, - data.node.textSelectionEnd.toString(), - ), - ) - - add( - EntityExtra( - ActionEntity.EXTRA_ACCESSIBILITY_IS_EDITABLE, - data.node.isEditable.toString(), - ), - ) + data.text?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TEXT, it)) } - data.node.className?.let { + data.className?.let { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME, @@ -885,7 +837,7 @@ object ActionDataEntityMapper { ) } - data.node.viewResourceId?.let { + data.viewResourceId?.let { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID, @@ -894,7 +846,7 @@ object ActionDataEntityMapper { ) } - data.node.uniqueId?.let { + data.uniqueId?.let { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID, @@ -903,11 +855,11 @@ object ActionDataEntityMapper { ) } - if (data.node.actions.isNotEmpty()) { + if (data.nodeActions.isNotEmpty()) { add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS, - data.node.actions.joinToString(","), + data.nodeActions.joinToString(","), ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index 7d34983a0d..ac94158e21 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -786,6 +786,19 @@ class CreateActionDelegate( } return null } + + ActionId.INTERACT_UI_ELEMENT -> { + val oldAction = if (oldData is ActionData.InteractUiElement) { + oldData + } else { + null + } + + return navigate( + "config_interact_ui_element_action", + NavDestination.InteractUiElement(oldAction), + ) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 9734435728..3418b6edbc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -65,6 +65,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import splitties.bitflags.withFlag @@ -799,6 +800,20 @@ class PerformActionsUseCaseImpl( authorizationHeader = action.authorizationHeader, ) } + + is ActionData.InteractUiElement -> { + if (accessibilityService.activeWindowPackage.first() != action.packageName) { + // TODO + } + + result = accessibilityService.performActionOnNode( + findNode = { node -> + // TODO compare other values + node.uniqueId == action.uniqueId + }, + performAction = { AccessibilityNodeAction(action = action.nodeAction) }, + ) + } } when (result) { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt new file mode 100644 index 0000000000..df9eb04b45 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt @@ -0,0 +1,80 @@ +package io.github.sds100.keymapper.actions.uielement + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.withStateAtLeast +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.databinding.FragmentComposeBinding +import io.github.sds100.keymapper.util.Inject +import io.github.sds100.keymapper.util.launchRepeatOnLifecycle +import io.github.sds100.keymapper.util.viewLifecycleScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json + +class InteractUiElementFragment : Fragment() { + + companion object { + const val EXTRA_ACTION = "extra_action" + } + + private val args: InteractUiElementFragmentArgs by navArgs() + + private val viewModel by viewModels { + Inject.interactUiElementViewModel(requireContext()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + args.action?.let { argsAction -> viewModel.loadAction(Json.decodeFromString(argsAction)) } + + launchRepeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.returnAction.collectLatest { action -> + viewLifecycleScope.launch { + withStateAtLeast(Lifecycle.State.RESUMED) { + setFragmentResult( + args.requestKey, + bundleOf(EXTRA_ACTION to Json.encodeToString(action)), + ) + findNavController().navigateUp() + } + } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + FragmentComposeBinding.inflate(inflater, container, false).apply { + composeView.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + KeyMapperTheme { + InteractUiElementScreen( + viewModel = viewModel, + navigateBack = findNavController()::navigateUp, + ) + } + } + } + return this.root + } + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt new file mode 100644 index 0000000000..aa702d54e1 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -0,0 +1,169 @@ +package io.github.sds100.keymapper.actions.uielement + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.drawable +import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo + +@Composable +fun InteractUiElementScreen( + modifier: Modifier = Modifier, + viewModel: InteractUiElementViewModel, + navigateBack: () -> Unit, +) { + val snackbarHostState = SnackbarHostState() + val recordState by viewModel.recordState.collectAsStateWithLifecycle() + val selectedElementState by viewModel.selectedElementState.collectAsStateWithLifecycle() + + InteractUiElementScreen( + modifier = modifier, + recordState = recordState, + selectedElementState = selectedElementState, + onBackClick = navigateBack, + onDoneClick = { viewModel.onDoneClick() }, + snackbarHostState = snackbarHostState, + ) +} + +@Composable +private fun InteractUiElementScreen( + modifier: Modifier = Modifier, + recordState: State, + selectedElementState: SelectedUiElementState?, + onBackClick: () -> Unit = {}, + onDoneClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = SnackbarHostState(), +) { + BackHandler(onBack = onBackClick) + + Scaffold( + modifier.displayCutoutPadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + BottomAppBar(actions = { + IconButton(onClick = onBackClick) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + stringResource(R.string.action_go_back), + ) + } + }, floatingActionButton = { + if (selectedElementState != null) { + ExtendedFloatingActionButton( + onClick = onDoneClick, + text = { Text(stringResource(R.string.button_done)) }, + icon = { + Icon(Icons.Rounded.Check, stringResource(R.string.button_done)) + }, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), + ) + } + }) + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + + ) { + Column { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp, + ), + text = stringResource(R.string.action_interact_ui_element_title), + style = MaterialTheme.typography.titleLarge, + ) + } + } + } +} + +@Preview +@Composable +private fun PreviewEmpty() { + KeyMapperTheme { + InteractUiElementScreen( + recordState = State.Data(RecordUiElementState.Empty), + selectedElementState = null, + ) + } +} + +@Preview +@Composable +private fun PreviewLoading() { + KeyMapperTheme { + InteractUiElementScreen( + recordState = State.Loading, + selectedElementState = null, + ) + } +} + +@Preview +@Composable +private fun PreviewSelectedElement() { + val appIcon = LocalContext.current.drawable(R.mipmap.ic_launcher_round) + + KeyMapperTheme { + InteractUiElementScreen( + recordState = State.Data(RecordUiElementState.Recorded(0)), + selectedElementState = SelectedUiElementState( + description = "Test", + appName = "Test App", + appIcon = ComposeIconInfo.Drawable(appIcon), + nodeText = "Test Node", + nodeClassName = "Test Class", + nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", + nodeUniqueId = "123", + interactionTypes = listOf(), + selectedInteraction = 0, + ), + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt new file mode 100644 index 0000000000..d6482184d4 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt @@ -0,0 +1,73 @@ +package io.github.sds100.keymapper.actions.uielement + +import android.graphics.drawable.Drawable +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository +import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState +import io.github.sds100.keymapper.system.accessibility.ServiceAdapter +import io.github.sds100.keymapper.system.apps.PackageManagerAdapter +import io.github.sds100.keymapper.util.Result +import io.github.sds100.keymapper.util.ServiceEvent +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.mapData +import io.github.sds100.keymapper.util.onFailure +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update + +class InteractUiElementUseCaseImpl( + private val serviceAdapter: ServiceAdapter, + private val nodeRepository: AccessibilityNodeRepository, + private val packageManagerAdapter: PackageManagerAdapter, +) : InteractUiElementUseCase { + override val recordState: MutableStateFlow = + MutableStateFlow(RecordAccessibilityNodeState.Idle) + + override val interactionCount: Flow> = + nodeRepository.nodes.map { state -> state.mapData { it.size } } + + override val interactedPackages: Flow>> = nodeRepository.nodes.map { state -> + state.mapData { nodes -> + nodes.map { it.packageName }.distinct() + } + } + + override fun getInteractionsByPackage(packageName: String): Flow>> { + return nodeRepository.nodes.map { state -> + state.mapData { nodes -> + nodes.filter { it.packageName == packageName } + } + } + } + + override fun getAppName(packageName: String): Result = packageManagerAdapter.getAppName(packageName) + + override fun getAppIcon(packageName: String): Result = packageManagerAdapter.getAppIcon(packageName) + + override suspend fun startRecording(): Result<*> { + // TODO show snackbar when accessibility service is disabled error + return serviceAdapter.send(ServiceEvent.StartRecordingTrigger) + } + + override suspend fun stopRecording() { + serviceAdapter.send(ServiceEvent.StopRecordingNodes).onFailure { + recordState.update { RecordAccessibilityNodeState.Idle } + } + } +} + +interface InteractUiElementUseCase { + val recordState: StateFlow + + val interactionCount: Flow> + val interactedPackages: Flow>> + fun getInteractionsByPackage(packageName: String): Flow>> + + fun getAppName(packageName: String): Result + fun getAppIcon(packageName: String): Result + + suspend fun startRecording(): Result<*> + suspend fun stopRecording() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt new file mode 100644 index 0000000000..b7f7493230 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -0,0 +1,160 @@ +package io.github.sds100.keymapper.actions.uielement + +import android.view.accessibility.AccessibilityNodeInfo +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.actions.ActionData +import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.dataOrNull +import io.github.sds100.keymapper.util.ui.ResourceProvider +import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.valueOrNull +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class InteractUiElementViewModel( + private val useCase: InteractUiElementUseCase, + private val resourceProvider: ResourceProvider, +) : ViewModel(), + ResourceProvider by resourceProvider { + + private val _returnAction: MutableSharedFlow = MutableSharedFlow() + val returnAction: SharedFlow = _returnAction + + val recordState: StateFlow> = combine( + useCase.recordState, + useCase.interactionCount, + ) { recordState, interactionCountState -> + val interactionCount = interactionCountState.dataOrNull() ?: return@combine State.Loading + + when (recordState) { + is RecordAccessibilityNodeState.CountingDown -> { + val mins = recordState.timeLeft / 60 + val secs = recordState.timeLeft % 60 + + State.Data( + RecordUiElementState.CountingDown( + timeRemaining = "$mins:$secs", + interactionCount = interactionCount, + ), + ) + } + + RecordAccessibilityNodeState.Idle -> { + if (interactionCount == 0) { + State.Data(RecordUiElementState.Empty) + } else { + State.Data(RecordUiElementState.Recorded(interactionCount = interactionCount)) + } + } + } + }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + + private val _selectedElementState = MutableStateFlow(null) + val selectedElementState: StateFlow = + _selectedElementState.asStateFlow() + + fun loadAction(action: ActionData.InteractUiElement) { + viewModelScope.launch { + val appName = useCase.getAppName(action.packageName).valueOrNull() ?: action.packageName + val appIcon = useCase.getAppIcon(action.packageName).valueOrNull() + ?.let { ComposeIconInfo.Drawable(it) } + + val newState = SelectedUiElementState( + description = action.description, + appName = appName, + appIcon = appIcon, + nodeText = action.text ?: action.contentDescription, + nodeClassName = action.className, + nodeViewResourceId = action.viewResourceId, + nodeUniqueId = action.uniqueId, + interactionTypes = action.nodeActions, + selectedInteraction = action.nodeAction, + ) + + _selectedElementState.update { newState } + } + } + + fun onDoneClick() { + val selectedElementState = _selectedElementState.value + if (selectedElementState == null) { + return + } + + val action = ActionData.InteractUiElement( + description = selectedElementState.description, + nodeAction = selectedElementState.selectedInteraction, + packageName = selectedElementState.appName, + text = selectedElementState.nodeText, + contentDescription = selectedElementState.nodeText, + className = selectedElementState.nodeClassName, + viewResourceId = selectedElementState.nodeViewResourceId, + uniqueId = selectedElementState.nodeUniqueId, + nodeActions = selectedElementState.interactionTypes, + ) + + _returnAction.tryEmit(action) + } + + private fun getNodeActionName(nodeAction: Int): String { + return when (nodeAction) { + AccessibilityNodeInfo.ACTION_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) + AccessibilityNodeInfo.ACTION_LONG_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_long_click) + AccessibilityNodeInfo.ACTION_FOCUS -> getString(R.string.action_interact_ui_element_interaction_type_focus) + AccessibilityNodeInfo.ACTION_SELECT -> getString(R.string.action_interact_ui_element_interaction_type_select) + AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_forward) + AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_backward) + AccessibilityNodeInfo.ACTION_EXPAND -> getString(R.string.action_interact_ui_element_interaction_type_expand) + AccessibilityNodeInfo.ACTION_COLLAPSE -> getString(R.string.action_interact_ui_element_interaction_type_collapse) + else -> getString( + R.string.action_interact_ui_element_interaction_type_unknown, + nodeAction, + ) + } + } + + @Suppress("UNCHECKED_CAST") + class Factory( + private val useCase: InteractUiElementUseCase, + private val resourceProvider: ResourceProvider, + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return InteractUiElementViewModel(useCase, resourceProvider) as T + } + } +} + +data class SelectedUiElementState( + val description: String, + val appName: String, + val appIcon: ComposeIconInfo.Drawable?, + val nodeText: String?, + val nodeClassName: String?, + val nodeViewResourceId: String?, + val nodeUniqueId: String?, + val interactionTypes: List, + val selectedInteraction: Int, +) + +sealed class RecordUiElementState { + data class Recorded(val interactionCount: Int) : RecordUiElementState() + + data class CountingDown( + val timeRemaining: String, + val interactionCount: Int, + ) : RecordUiElementState() + + data object Empty : RecordUiElementState() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index f480ef56e6..8792942277 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -1,5 +1,11 @@ package io.github.sds100.keymapper.data.entities +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Serializable +@Parcelize data class AccessibilityNodeEntity( val id: Long = 0L, val parentId: Long? = null, @@ -19,4 +25,4 @@ data class AccessibilityNodeEntity( * this node. */ val userInteractedActionId: Int?, -) +) : Parcelable diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index 2160feaf75..4385e0b3a4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -94,12 +94,7 @@ data class ActionEntity( const val EXTRA_ACCESSIBILITY_PACKAGE_NAME = "extra_accessibility_package_name" const val EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION = "extra_accessibility_content_description" - const val EXTRA_ACCESSIBILITY_IS_FOCUSED = "extra_accessibility_is_focused" const val EXTRA_ACCESSIBILITY_TEXT = "extra_accessibility_text" - const val EXTRA_ACCESSIBILITY_TEXT_SELECTION_START = - "extra_accessibility_text_selection_start" - const val EXTRA_ACCESSIBILITY_TEXT_SELECTION_END = "extra_accessibility_text_selection_end" - const val EXTRA_ACCESSIBILITY_IS_EDITABLE = "extra_accessibility_is_editable" const val EXTRA_ACCESSIBILITY_CLASS_NAME = "extra_accessibility_class_name" const val EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID = "extra_accessibility_view_resource_id" const val EXTRA_ACCESSIBILITY_UNIQUE_ID = "extra_accessibility_unique_id" diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityEventModel.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityEventModel.kt deleted file mode 100644 index 3e0675d83d..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityEventModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.sds100.keymapper.system.accessibility - -/** - * Created by sds100 on 27/07/2021. - */ -data class AccessibilityEventModel(val eventTime: Long, val eventType: Int) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index 71cb54bcf2..fb64958011 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -1,54 +1,68 @@ package io.github.sds100.keymapper.system.accessibility -import android.accessibilityservice.AccessibilityService import android.os.Build +import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent -import io.github.sds100.keymapper.ServiceLocator import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch class AccessibilityNodeRecorder( - private val service: AccessibilityService, - private val coroutineScope: CoroutineScope, + private val nodeRepository: AccessibilityNodeRepository, ) { companion object { private const val RECORD_DURATION = 60000L } - private val nodeRepository: AccessibilityNodeRepository by lazy { - ServiceLocator.accessibilityNodeRepository(service) - } - - private var recordJob: Job? = null - private val _isRecording = MutableStateFlow(false) - val isRecording = _isRecording.asStateFlow() + private val timerLock = Any() + private var timer: CountDownTimer? = null + private val _recordState: MutableStateFlow = + MutableStateFlow(RecordAccessibilityNodeState.Idle) + val recordState = _recordState.asStateFlow() fun startRecording() { - _isRecording.update { true } - recordJob?.cancel() - recordJob = recordJob() + synchronized(timerLock) { + timer?.cancel() + timer = object : CountDownTimer(RECORD_DURATION, 1000) { + + override fun onTick(millisUntilFinished: Long) { + _recordState.update { + RecordAccessibilityNodeState.CountingDown( + timeLeft = (millisUntilFinished / 1000).toInt(), + ) + } + } + + override fun onFinish() { + _recordState.update { RecordAccessibilityNodeState.Idle } + } + } + + timer!!.start() + } } fun stopRecording() { - recordJob?.cancel() - recordJob = null - _isRecording.update { false } + synchronized(timerLock) { + timer?.cancel() + timer = null + _recordState.update { RecordAccessibilityNodeState.Idle } + } } fun onAccessibilityEvent(event: AccessibilityEvent) { - if (!isRecording.value) { + if (_recordState.value is RecordAccessibilityNodeState.Idle) { return } val source = event.source ?: return + if (source.actionList.isNullOrEmpty()) { + return + } + val entity = AccessibilityNodeEntity( packageName = event.packageName.toString(), @@ -62,13 +76,16 @@ class AccessibilityNodeRecorder( null }, actions = source.actionList?.map { it.id } ?: emptyList(), + userInteractedActionId = event.action, ) nodeRepository.insert(entity) } - private fun recordJob() = coroutineScope.launch { - delay(RECORD_DURATION) - _isRecording.update { false } + fun teardown() { + synchronized(timerLock) { + timer?.cancel() + timer = null + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt index 7fff55c865..773486c357 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityUtils.kt @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.system.accessibility import android.os.Build -import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo /** @@ -48,5 +47,3 @@ fun AccessibilityNodeInfo.toModel(): AccessibilityNodeModel = AccessibilityNodeM viewResourceId = viewIdResourceName, actions = actionList.map { it.id }, ) - -fun AccessibilityEvent.toModel(): AccessibilityEventModel = AccessibilityEventModel(eventTime, eventType) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index 7ab9aea436..bdbcc376fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -11,6 +11,7 @@ import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.mappings.FingerprintGestureType import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase @@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop @@ -73,6 +75,7 @@ abstract class BaseAccessibilityServiceController( private val suAdapter: SuAdapter, private val inputMethodAdapter: InputMethodAdapter, private val settingsRepository: PreferenceRepository, + private val nodeRepository: AccessibilityNodeRepository, ) { companion object { @@ -102,7 +105,7 @@ abstract class BaseAccessibilityServiceController( ) private val accessibilityNodeRecorder: AccessibilityNodeRecorder = - AccessibilityNodeRecorder(service, coroutineScope) + AccessibilityNodeRecorder(nodeRepository) private var recordingTriggerJob: Job? = null private val recordingTrigger: Boolean @@ -260,12 +263,19 @@ abstract class BaseAccessibilityServiceController( } }.launchIn(coroutineScope) + coroutineScope.launch { + accessibilityNodeRecorder.recordState.collectLatest { state -> + outputEvents.emit(ServiceEvent.OnRecordNodeStateChanged(state)) + } + } + val imeInputFocusEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + // TODO // coroutineScope.launch { // combine( // changeImeOnInputFocusFlow, @@ -320,6 +330,10 @@ abstract class BaseAccessibilityServiceController( } } + open fun onDestroy() { + accessibilityNodeRecorder.teardown() + } + open fun onConfigurationChanged(newConfig: Configuration) { } @@ -539,10 +553,9 @@ abstract class BaseAccessibilityServiceController( is ServiceEvent.TriggerKeyMap -> triggerKeyMapFromIntent(event.uid) is ServiceEvent.EnableInputMethod -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - accessibilityService.setInputMethodEnabled(event.imeId, true) + service.setInputMethodEnabled(event.imeId, true) } - is ServiceEvent.StartRecordingNodes -> { accessibilityNodeRecorder.startRecording() } @@ -556,6 +569,7 @@ abstract class BaseAccessibilityServiceController( } private fun recordTriggerJob() = coroutineScope.launch { + // TODO use Kotlin timer. repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> if (isActive) { val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index 35f9c4b55a..99205867b6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -255,6 +255,7 @@ class MyAccessibilityService : override fun onInterrupt() {} override fun onDestroy() { + controller?.onDestroy() controller = null lifecycleRegistry.currentState = Lifecycle.State.DESTROYED diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/RecordAccessibilityNodeState.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/RecordAccessibilityNodeState.kt new file mode 100644 index 0000000000..ea1e26d091 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/RecordAccessibilityNodeState.kt @@ -0,0 +1,15 @@ +package io.github.sds100.keymapper.system.accessibility + +import kotlinx.serialization.Serializable + +@Serializable +sealed class RecordAccessibilityNodeState { + data object Idle : RecordAccessibilityNodeState() + + data class CountingDown( + /** + * The time left in seconds + */ + val timeLeft: Int, + ) : RecordAccessibilityNodeState() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt index 41a4f69d47..adf8b49153 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt @@ -15,6 +15,8 @@ import io.github.sds100.keymapper.actions.sound.ChooseSoundFileUseCaseImpl import io.github.sds100.keymapper.actions.sound.ChooseSoundFileViewModel import io.github.sds100.keymapper.actions.swipescreen.SwipePickDisplayCoordinateViewModel import io.github.sds100.keymapper.actions.tapscreen.PickDisplayCoordinateViewModel +import io.github.sds100.keymapper.actions.uielement.InteractUiElementUseCaseImpl +import io.github.sds100.keymapper.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.backup.BackupRestoreMappingsUseCaseImpl import io.github.sds100.keymapper.constraints.ChooseConstraintViewModel @@ -229,6 +231,7 @@ object Inject { ), inputMethodAdapter = ServiceLocator.inputMethodAdapter(service), settingsRepository = ServiceLocator.settingsRepository(service), + nodeRepository = ServiceLocator.accessibilityNodeRepository(service), ) fun chooseBluetoothDeviceViewModel(ctx: Context): ChooseBluetoothDeviceViewModel.Factory = ChooseBluetoothDeviceViewModel.Factory( @@ -248,4 +251,15 @@ object Inject { ), ServiceLocator.resourceProvider(ctx), ) + + fun interactUiElementViewModel( + ctx: Context, + ): InteractUiElementViewModel.Factory = InteractUiElementViewModel.Factory( + InteractUiElementUseCaseImpl( + serviceAdapter = ServiceLocator.accessibilityServiceAdapter(ctx), + nodeRepository = ServiceLocator.accessibilityNodeRepository(ctx), + packageManagerAdapter = ServiceLocator.packageManagerAdapter(ctx), + ), + resourceProvider = ServiceLocator.resourceProvider(ctx), + ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt index 909f82097a..4bacac2040 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.util import android.os.Parcelable import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -78,4 +79,7 @@ sealed class ServiceEvent { @Serializable data object StopRecordingNodes : ServiceEvent() + + @Serializable + data class OnRecordNodeStateChanged(val state: RecordAccessibilityNodeState) : ServiceEvent() } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt index 4e3ede84f8..de4cb1bea8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavDestination.kt @@ -36,6 +36,7 @@ sealed class NavDestination { const val ID_CONFIG_KEY_MAP = "config_key_map" const val ID_SHIZUKU_SETTINGS = "shizuku_settings" const val ID_CONFIG_FLOATING_BUTTON = "config_floating_button" + const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" } data class ChooseApp( @@ -123,4 +124,8 @@ sealed class NavDestination { data class ConfigFloatingButton(val buttonUid: String?) : NavDestination() { override val id: String = ID_CONFIG_FLOATING_BUTTON } + + data class InteractUiElement(val action: ActionData.InteractUiElement?) : NavDestination() { + override val id: String = ID_INTERACT_UI_ELEMENT_ACTION + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt index eaf76d4b9d..67d57171b5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json /** @@ -225,6 +224,11 @@ fun NavigationViewModel.setupNavigation(fragment: Fragment) { is NavDestination.ConfigFloatingButton -> NavAppDirections.toConfigFloatingButton( destination.buttonUid, ) + + is NavDestination.InteractUiElement -> NavAppDirections.interactUiElement( + requestKey = requestKey, + action = destination.action?.let { Json.encodeToString(destination.action) }, + ) } fragment.findNavController().navigate(direction) diff --git a/app/src/main/res/navigation/nav_app.xml b/app/src/main/res/navigation/nav_app.xml index 4a89c3be6e..61eb7309de 100644 --- a/app/src/main/res/navigation/nav_app.xml +++ b/app/src/main/res/navigation/nav_app.xml @@ -386,4 +386,27 @@ app:argType="string" app:nullable="true" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d4e31a44c..75b2a0682f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1127,6 +1127,7 @@ Scroll backward Expand Collapse + Unknown: %d Text From 289b3c3160d14cc64378b5c8be2e44c1d59e1e0d Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 1 May 2025 18:04:21 +0200 Subject: [PATCH 09/69] #257 start/stop recording UI elements --- .../github/sds100/keymapper/KeyMapperApp.kt | 10 ++ .../uielement/InteractUiElementFragment.kt | 7 ++ .../uielement/InteractUiElementScreen.kt | 110 +++++++++++++++++- .../uielement/InteractUiElementUseCase.kt | 23 +++- .../uielement/InteractUiElementViewModel.kt | 39 ++++++- .../io/github/sds100/keymapper/util/Inject.kt | 7 +- 6 files changed, 185 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt index b5b3275dc6..162bbd5858 100644 --- a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt +++ b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication +import io.github.sds100.keymapper.actions.uielement.InteractUiElementController import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.logging.KeyMapperLoggingTree @@ -151,6 +152,15 @@ class KeyMapperApp : MultiDexApplication() { RecordTriggerController(appCoroutineScope, accessibilityServiceAdapter) } + val interactUiElementController by lazy { + InteractUiElementController( + appCoroutineScope, + accessibilityServiceAdapter, + ServiceLocator.accessibilityNodeRepository(this), + packageManagerAdapter, + ) + } + val autoGrantPermissionController by lazy { AutoGrantPermissionController( appCoroutineScope, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt index df9eb04b45..87f70c845d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt @@ -18,6 +18,7 @@ import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.databinding.FragmentComposeBinding import io.github.sds100.keymapper.util.Inject import io.github.sds100.keymapper.util.launchRepeatOnLifecycle +import io.github.sds100.keymapper.util.ui.showPopups import io.github.sds100.keymapper.util.viewLifecycleScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -77,4 +78,10 @@ class InteractUiElementFragment : Fragment() { return this.root } } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.showPopups(this, view) + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index aa702d54e1..616518e520 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -1,21 +1,28 @@ package io.github.sds100.keymapper.actions.uielement import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -27,11 +34,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo @@ -51,7 +60,8 @@ fun InteractUiElementScreen( recordState = recordState, selectedElementState = selectedElementState, onBackClick = navigateBack, - onDoneClick = { viewModel.onDoneClick() }, + onDoneClick = viewModel::onDoneClick, + onRecordClick = viewModel::onRecordClick, snackbarHostState = snackbarHostState, ) } @@ -63,6 +73,7 @@ private fun InteractUiElementScreen( selectedElementState: SelectedUiElementState?, onBackClick: () -> Unit = {}, onDoneClick: () -> Unit = {}, + onRecordClick: () -> Unit = {}, snackbarHostState: SnackbarHostState = SnackbarHostState(), ) { BackHandler(onBack = onBackClick) @@ -118,11 +129,108 @@ private fun InteractUiElementScreen( text = stringResource(R.string.action_interact_ui_element_title), style = MaterialTheme.typography.titleLarge, ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + RecordingSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = recordState, + onRecordClick = onRecordClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (selectedElementState != null) { + SelectedElementSection(modifier = Modifier.fillMaxWidth(), selectedElementState) + } } } } } +@Composable +private fun RecordingSection( + modifier: Modifier = Modifier, + state: State, + onRecordClick: () -> Unit = {}, +) { + Column(modifier = modifier) { + when (state) { + is State.Data -> { + RecordButton( + modifier = Modifier.fillMaxWidth(), + state = state.data, + onClick = onRecordClick, + ) + } + + State.Loading -> TODO() + } + } +} + +@Composable +private fun RecordButton( + modifier: Modifier, + state: RecordUiElementState, + onClick: () -> Unit, +) { + val text: String = when (state) { + is RecordUiElementState.Empty -> stringResource(R.string.action_interact_ui_element_start_recording) + is RecordUiElementState.Recorded -> stringResource(R.string.action_interact_ui_element_record_again) + is RecordUiElementState.CountingDown -> stringResource( + R.string.action_interact_ui_element_stop_recording, + state.timeRemaining, + ) + } + + if (state is RecordUiElementState.Recorded) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + colors = ButtonDefaults.outlinedButtonColors().copy( + contentColor = LocalCustomColorsPalette.current.red, + ), + border = BorderStroke(1.dp, color = LocalCustomColorsPalette.current.red), + ) { + Text( + text = text, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } else { + FilledTonalButton( + modifier = modifier, + onClick = onClick, + colors = ButtonDefaults.filledTonalButtonColors().copy( + containerColor = LocalCustomColorsPalette.current.red, + contentColor = LocalCustomColorsPalette.current.onRed, + ), + ) { + Text( + text = text, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun SelectedElementSection(modifier: Modifier = Modifier, state: SelectedUiElementState) { +} + @Preview @Composable private fun PreviewEmpty() { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt index d6482184d4..79688b1629 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt @@ -11,13 +11,18 @@ import io.github.sds100.keymapper.util.ServiceEvent import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.mapData import io.github.sds100.keymapper.util.onFailure +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -class InteractUiElementUseCaseImpl( +class InteractUiElementController( + private val coroutineScope: CoroutineScope, private val serviceAdapter: ServiceAdapter, private val nodeRepository: AccessibilityNodeRepository, private val packageManagerAdapter: PackageManagerAdapter, @@ -34,6 +39,13 @@ class InteractUiElementUseCaseImpl( } } + init { + serviceAdapter.eventReceiver + .filterIsInstance() + .onEach { event -> recordState.update { event.state } } + .launchIn(coroutineScope) + } + override fun getInteractionsByPackage(packageName: String): Flow>> { return nodeRepository.nodes.map { state -> state.mapData { nodes -> @@ -47,8 +59,7 @@ class InteractUiElementUseCaseImpl( override fun getAppIcon(packageName: String): Result = packageManagerAdapter.getAppIcon(packageName) override suspend fun startRecording(): Result<*> { - // TODO show snackbar when accessibility service is disabled error - return serviceAdapter.send(ServiceEvent.StartRecordingTrigger) + return serviceAdapter.send(ServiceEvent.StartRecordingNodes) } override suspend fun stopRecording() { @@ -56,6 +67,10 @@ class InteractUiElementUseCaseImpl( recordState.update { RecordAccessibilityNodeState.Idle } } } + + override fun startService(): Boolean { + return serviceAdapter.start() + } } interface InteractUiElementUseCase { @@ -70,4 +85,6 @@ interface InteractUiElementUseCase { suspend fun startRecording(): Result<*> suspend fun stopRecording() + + fun startService(): Boolean } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index b7f7493230..014731b87b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -7,9 +7,15 @@ import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.R import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState +import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull +import io.github.sds100.keymapper.util.ifIsData +import io.github.sds100.keymapper.util.onFailure +import io.github.sds100.keymapper.util.ui.PopupViewModel +import io.github.sds100.keymapper.util.ui.PopupViewModelImpl import io.github.sds100.keymapper.util.ui.ResourceProvider +import io.github.sds100.keymapper.util.ui.ViewModelHelper import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.valueOrNull import kotlinx.coroutines.flow.MutableSharedFlow @@ -27,7 +33,8 @@ class InteractUiElementViewModel( private val useCase: InteractUiElementUseCase, private val resourceProvider: ResourceProvider, ) : ViewModel(), - ResourceProvider by resourceProvider { + ResourceProvider by resourceProvider, + PopupViewModel by PopupViewModelImpl() { private val _returnAction: MutableSharedFlow = MutableSharedFlow() val returnAction: SharedFlow = _returnAction @@ -108,6 +115,36 @@ class InteractUiElementViewModel( _returnAction.tryEmit(action) } + fun onRecordClick() { + recordState.value.ifIsData { recordState -> + viewModelScope.launch { + when (recordState) { + is RecordUiElementState.CountingDown -> useCase.stopRecording() + RecordUiElementState.Empty -> startRecording() + is RecordUiElementState.Recorded -> startRecording() + } + } + } + } + + private suspend fun startRecording() { + useCase.startRecording().onFailure { error -> + if (error == Error.AccessibilityServiceDisabled) { + ViewModelHelper.handleAccessibilityServiceStoppedDialog( + this, + this, + startService = { useCase.startService() }, + ) + } else if (error == Error.AccessibilityServiceCrashed) { + ViewModelHelper.handleAccessibilityServiceCrashedDialog( + this, + this, + restartService = { useCase.startService() }, + ) + } + } + } + private fun getNodeActionName(nodeAction: Int): String { return when (nodeAction) { AccessibilityNodeInfo.ACTION_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt index adf8b49153..6bb6786364 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt @@ -15,7 +15,6 @@ import io.github.sds100.keymapper.actions.sound.ChooseSoundFileUseCaseImpl import io.github.sds100.keymapper.actions.sound.ChooseSoundFileViewModel import io.github.sds100.keymapper.actions.swipescreen.SwipePickDisplayCoordinateViewModel import io.github.sds100.keymapper.actions.tapscreen.PickDisplayCoordinateViewModel -import io.github.sds100.keymapper.actions.uielement.InteractUiElementUseCaseImpl import io.github.sds100.keymapper.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.api.KeyEventRelayServiceWrapper import io.github.sds100.keymapper.backup.BackupRestoreMappingsUseCaseImpl @@ -255,11 +254,7 @@ object Inject { fun interactUiElementViewModel( ctx: Context, ): InteractUiElementViewModel.Factory = InteractUiElementViewModel.Factory( - InteractUiElementUseCaseImpl( - serviceAdapter = ServiceLocator.accessibilityServiceAdapter(ctx), - nodeRepository = ServiceLocator.accessibilityNodeRepository(ctx), - packageManagerAdapter = ServiceLocator.packageManagerAdapter(ctx), - ), + (ctx.applicationContext as KeyMapperApp).interactUiElementController, resourceProvider = ServiceLocator.resourceProvider(ctx), ) } From a55808ac92b3f19877b5366c0fb1777f631d30fd Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 3 May 2025 15:23:53 +0200 Subject: [PATCH 10/69] feat: rename tap screen actions inside key maps --- CHANGELOG.md | 4 ++++ .../java/io/github/sds100/keymapper/actions/ActionUiHelper.kt | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b43105a25..7e501ffdd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - #699 Time constraints ⏰ +## Changed + +- Rename tap screen actions inside key maps. + ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index be4ebb39e4..e782916e4b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -356,7 +356,7 @@ class ActionUiHelper( } else { getString( R.string.description_tap_coordinate_with_description, - arrayOf(action.x, action.y, action.description), + action.description, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75b2a0682f..beb6c3c235 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,8 +98,8 @@ Input %s through shell Input %s%s from %s Open %s - Tap coordinates %d, %d - Tap coordinates %d, %d (%s) + Tap screen (%d, %d) + Tap screen (%s) Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms (%s) %s with %d finger(s) on coordinates %d/%d with a pinch distance of %dpx in %dms From 03b43b6637fef70e2f815fb8b1320473580df44e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 3 May 2025 15:23:53 +0200 Subject: [PATCH 11/69] feat: rename tap screen actions inside key maps --- CHANGELOG.md | 4 ++++ .../java/io/github/sds100/keymapper/actions/ActionUiHelper.kt | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b43105a25..7e501ffdd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - #699 Time constraints ⏰ +## Changed + +- Rename tap screen actions inside key maps. + ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index 755353b04d..9aa24cbbed 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -356,7 +356,7 @@ class ActionUiHelper( } else { getString( R.string.description_tap_coordinate_with_description, - arrayOf(action.x, action.y, action.description), + action.description, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46985421d3..f4747ad28e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,8 +98,8 @@ Input %s through shell Input %s%s from %s Open %s - Tap coordinates %d, %d - Tap coordinates %d, %d (%s) + Tap screen (%d, %d) + Tap screen (%s) Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms Swipe with %d finger(s) from coordinates %d/%d to %d/%d in %dms (%s) %s with %d finger(s) on coordinates %d/%d with a pinch distance of %dpx in %dms From 21d00621190118d4878e487a44ec8c9a5650a5c6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 3 May 2025 16:55:20 +0200 Subject: [PATCH 12/69] #257 feat: show teh number of interactions --- .../uielement/InteractUiElementScreen.kt | 130 ++++++++++++++++-- .../util/ui/compose/icons/AdGroup.kt | 78 +++++++++++ 2 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/AdGroup.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 616518e520..91d1e88d60 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.actions.uielement import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -11,9 +12,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExtendedFloatingActionButton @@ -21,6 +24,8 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -28,11 +33,15 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -44,6 +53,8 @@ import io.github.sds100.keymapper.compose.LocalCustomColorsPalette import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.icons.AdGroup +import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons @Composable fun InteractUiElementScreen( @@ -90,14 +101,34 @@ private fun InteractUiElementScreen( ) } }, floatingActionButton = { - if (selectedElementState != null) { + val isEnabled = selectedElementState != null + val containerColor = if (isEnabled) { + FloatingActionButtonDefaults.containerColor + } else { + FloatingActionButtonDefaults.containerColor.copy(alpha = 0.5f) + } + + val contentColor = if (isEnabled) { + MaterialTheme.colorScheme.contentColorFor(containerColor) + } else { + MaterialTheme.colorScheme.contentColorFor(containerColor).copy(alpha = 0.5f) + } + + CompositionLocalProvider( + LocalContentColor provides contentColor, + ) { ExtendedFloatingActionButton( - onClick = onDoneClick, + onClick = if (isEnabled) { + onDoneClick + } else { + {} + }, text = { Text(stringResource(R.string.button_done)) }, icon = { Icon(Icons.Rounded.Check, stringResource(R.string.button_done)) }, elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), + containerColor = containerColor, ) } }) @@ -167,6 +198,20 @@ private fun RecordingSection( Column(modifier = modifier) { when (state) { is State.Data -> { + val interactionCount: Int = when (state.data) { + is RecordUiElementState.CountingDown -> state.data.interactionCount + is RecordUiElementState.Recorded -> state.data.interactionCount + RecordUiElementState.Empty -> 0 + } + + InteractionCountBox( + modifier = Modifier.fillMaxWidth(), + interactionCount = interactionCount, + onClick = {}, + ) + + Spacer(modifier = Modifier.height(8.dp)) + RecordButton( modifier = Modifier.fillMaxWidth(), state = state.data, @@ -174,7 +219,60 @@ private fun RecordingSection( ) } - State.Loading -> TODO() + State.Loading -> { + Spacer(modifier = Modifier.height(16.dp)) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +private fun InteractionCountBox( + modifier: Modifier = Modifier, + interactionCount: Int, + onClick: () -> Unit, +) { + val enabled = interactionCount > 0 + + val color = if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + } + + Surface(modifier = modifier, onClick = onClick, enabled = enabled) { + CompositionLocalProvider( + LocalContentColor provides color, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(16.dp)) + Icon(imageVector = KeyMapperIcons.AdGroup, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + pluralStringResource( + R.plurals.action_interact_ui_element_interactions_detected, + interactionCount, + interactionCount, + ), + style = MaterialTheme.typography.bodyLarge, + ) + + Text( + stringResource(R.string.action_interact_ui_element_choose_interaction), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + Icon(imageVector = Icons.Rounded.ChevronRight, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + } } } } @@ -229,6 +327,8 @@ private fun RecordButton( @Composable private fun SelectedElementSection(modifier: Modifier = Modifier, state: SelectedUiElementState) { + Column(modifier = modifier) { + } } @Preview @@ -242,17 +342,6 @@ private fun PreviewEmpty() { } } -@Preview -@Composable -private fun PreviewLoading() { - KeyMapperTheme { - InteractUiElementScreen( - recordState = State.Loading, - selectedElementState = null, - ) - } -} - @Preview @Composable private fun PreviewSelectedElement() { @@ -260,7 +349,7 @@ private fun PreviewSelectedElement() { KeyMapperTheme { InteractUiElementScreen( - recordState = State.Data(RecordUiElementState.Recorded(0)), + recordState = State.Data(RecordUiElementState.Recorded(3)), selectedElementState = SelectedUiElementState( description = "Test", appName = "Test App", @@ -275,3 +364,14 @@ private fun PreviewSelectedElement() { ) } } + +@Preview +@Composable +private fun PreviewLoading() { + KeyMapperTheme { + InteractUiElementScreen( + recordState = State.Loading, + selectedElementState = null, + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/AdGroup.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/AdGroup.kt new file mode 100644 index 0000000000..05f0f4db8b --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/icons/AdGroup.kt @@ -0,0 +1,78 @@ +package io.github.sds100.keymapper.util.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.AdGroup: ImageVector + get() { + if (_AdGroup != null) { + return _AdGroup!! + } + _AdGroup = ImageVector.Builder( + name = "AdGroup", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path(fill = SolidColor(Color(0xFF000000))) { + moveTo(320f, 640f) + lineTo(800f, 640f) + quadTo(800f, 640f, 800f, 640f) + quadTo(800f, 640f, 800f, 640f) + lineTo(800f, 240f) + lineTo(320f, 240f) + lineTo(320f, 640f) + quadTo(320f, 640f, 320f, 640f) + quadTo(320f, 640f, 320f, 640f) + close() + moveTo(320f, 720f) + quadTo(287f, 720f, 263.5f, 696.5f) + quadTo(240f, 673f, 240f, 640f) + lineTo(240f, 160f) + quadTo(240f, 127f, 263.5f, 103.5f) + quadTo(287f, 80f, 320f, 80f) + lineTo(800f, 80f) + quadTo(833f, 80f, 856.5f, 103.5f) + quadTo(880f, 127f, 880f, 160f) + lineTo(880f, 640f) + quadTo(880f, 673f, 856.5f, 696.5f) + quadTo(833f, 720f, 800f, 720f) + lineTo(320f, 720f) + close() + moveTo(160f, 880f) + quadTo(127f, 880f, 103.5f, 856.5f) + quadTo(80f, 833f, 80f, 800f) + lineTo(80f, 240f) + lineTo(160f, 240f) + lineTo(160f, 800f) + quadTo(160f, 800f, 160f, 800f) + quadTo(160f, 800f, 160f, 800f) + lineTo(720f, 800f) + lineTo(720f, 880f) + lineTo(160f, 880f) + close() + moveTo(320f, 160f) + quadTo(320f, 160f, 320f, 160f) + quadTo(320f, 160f, 320f, 160f) + lineTo(320f, 640f) + quadTo(320f, 640f, 320f, 640f) + quadTo(320f, 640f, 320f, 640f) + lineTo(320f, 640f) + quadTo(320f, 640f, 320f, 640f) + quadTo(320f, 640f, 320f, 640f) + lineTo(320f, 160f) + quadTo(320f, 160f, 320f, 160f) + quadTo(320f, 160f, 320f, 160f) + close() + } + }.build() + + return _AdGroup!! + } + +@Suppress("ObjectPropertyName") +private var _AdGroup: ImageVector? = null From 22c3226689158a8f2582f5cd427f6cb3d6f7a718 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 3 May 2025 17:33:57 +0200 Subject: [PATCH 13/69] #257 recording nodes works --- .../uielement/InteractUiElementScreen.kt | 38 ++++-------- .../AccessibilityNodeRecorder.kt | 2 +- .../BaseAccessibilityServiceController.kt | 62 ++++++++++--------- 3 files changed, 45 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 91d1e88d60..72b405b0ef 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -101,34 +100,14 @@ private fun InteractUiElementScreen( ) } }, floatingActionButton = { - val isEnabled = selectedElementState != null - val containerColor = if (isEnabled) { - FloatingActionButtonDefaults.containerColor - } else { - FloatingActionButtonDefaults.containerColor.copy(alpha = 0.5f) - } - - val contentColor = if (isEnabled) { - MaterialTheme.colorScheme.contentColorFor(containerColor) - } else { - MaterialTheme.colorScheme.contentColorFor(containerColor).copy(alpha = 0.5f) - } - - CompositionLocalProvider( - LocalContentColor provides contentColor, - ) { + if (selectedElementState != null) { ExtendedFloatingActionButton( - onClick = if (isEnabled) { - onDoneClick - } else { - {} - }, + onClick = onDoneClick, text = { Text(stringResource(R.string.button_done)) }, icon = { Icon(Icons.Rounded.Check, stringResource(R.string.button_done)) }, elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), - containerColor = containerColor, ) } }) @@ -242,15 +221,21 @@ private fun InteractionCountBox( MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) } - Surface(modifier = modifier, onClick = onClick, enabled = enabled) { + Surface( + modifier = modifier, + onClick = onClick, + enabled = enabled, + shape = MaterialTheme.shapes.medium, + ) { CompositionLocalProvider( LocalContentColor provides color, ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(modifier = Modifier.width(16.dp)) Icon(imageVector = KeyMapperIcons.AdGroup, contentDescription = null) Spacer(modifier = Modifier.width(16.dp)) Column(modifier = Modifier.weight(1f)) { @@ -271,7 +256,6 @@ private fun InteractionCountBox( Spacer(modifier = Modifier.width(16.dp)) Icon(imageVector = Icons.Rounded.ChevronRight, contentDescription = null) - Spacer(modifier = Modifier.width(16.dp)) } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index fb64958011..d031696e04 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -66,7 +66,7 @@ class AccessibilityNodeRecorder( val entity = AccessibilityNodeEntity( packageName = event.packageName.toString(), - text = source.text.firstOrNull()?.toString(), + text = source.text?.toString(), contentDescription = source.contentDescription?.toString(), className = source.className?.toString(), viewResourceId = source.viewIdResourceName, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index bdbcc376fc..d2206d813e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -271,37 +273,39 @@ abstract class BaseAccessibilityServiceController( val imeInputFocusEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED + val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED or - AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED - - // TODO -// coroutineScope.launch { -// combine( -// changeImeOnInputFocusFlow, -// accessibilityNodeRecorder.isRecording -// ) { changeImeOnInputFocus, isRecordingNodes -> -// serviceEventTypes.update { eventTypes -> -// var newEventTypes = eventTypes -// -// -// -// newEventTypes -// } -// -// }.collect() -// } -// changeImeOnInputFocusFlow.onEach { changeImeOnInputFocus -> -// if (changeImeOnInputFocus) { -// serviceEventTypes.value = serviceEventTypes.value -// .withFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) -// .withFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) -// } else { -// serviceEventTypes.value = serviceEventTypes.value -// .minusFlag(AccessibilityEvent.TYPE_VIEW_FOCUSED) -// .minusFlag(AccessibilityEvent.TYPE_VIEW_CLICKED) -// } -// }.launchIn(coroutineScope) + AccessibilityEvent.TYPE_VIEW_LONG_CLICKED or + AccessibilityEvent.TYPE_VIEW_SELECTED or + AccessibilityEvent.TYPE_VIEW_SCROLLED + + coroutineScope.launch { + combine( + changeImeOnInputFocusFlow, + accessibilityNodeRecorder.recordState, + ) { changeImeOnInputFocus, recordState -> + + serviceEventTypes.update { eventTypes -> + var newEventTypes = eventTypes + + if (!changeImeOnInputFocus && recordState == RecordAccessibilityNodeState.Idle) { + newEventTypes = + newEventTypes and (imeInputFocusEvents or recordNodeEvents).inv() + } else { + if (changeImeOnInputFocus) { + newEventTypes = newEventTypes or imeInputFocusEvents + } + + if (recordState is RecordAccessibilityNodeState.CountingDown) { + newEventTypes = newEventTypes or recordNodeEvents + } + } + + newEventTypes + } + }.collect() + } } open fun onServiceConnected() { From bb6ccba697b555f945cf2acd29f24031e271692d Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 3 May 2025 21:44:36 +0200 Subject: [PATCH 14/69] #257 list recorded apps --- app/build.gradle | 10 +- .../keymapper/actions/ChooseActionScreen.kt | 86 ++----- .../uielement/InteractUiElementFragment.kt | 3 + .../uielement/InteractUiElementScreen.kt | 85 +++++-- .../uielement/InteractUiElementViewModel.kt | 43 ++++ .../keymapper/system/apps/ChooseAppScreen.kt | 216 ++++++++++++++++++ .../util/ui/compose/SearchAppBarActions.kt | 90 ++++++++ .../util/ui/compose/SimpleListItem.kt | 107 ++++++--- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 513 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/system/apps/ChooseAppScreen.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SearchAppBarActions.kt diff --git a/app/build.gradle b/app/build.gradle index c7ed7dc671..e442585f81 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,7 +165,7 @@ dependencies { compileOnly project(":systemstubs") - def room_version = "2.6.1" + def room_version = "2.7.1" def coroutinesVersion = "1.9.0" def nav_version = '2.8.9' def epoxy_version = "4.6.2" @@ -179,7 +179,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0" // random stuff - implementation "com.google.android.material:material:1.13.0-alpha12" + implementation "com.google.android.material:material:1.13.0-alpha13" implementation "com.github.salomonbrys.kotson:kotson:2.5.0" implementation "com.airbnb.android:epoxy:$epoxy_version" implementation "com.airbnb.android:epoxy-databinding:$epoxy_version" @@ -192,7 +192,7 @@ dependencies { implementation "dev.rikka.shizuku:api:$shizuku_version" implementation "dev.rikka.shizuku:provider:$shizuku_version" implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" - proImplementation 'com.revenuecat.purchases:purchases:8.15.0' + proImplementation 'com.revenuecat.purchases:purchases:8.17.0' proImplementation "com.airbnb.android:lottie-compose:6.6.3" implementation("com.squareup.okhttp3:okhttp:4.12.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") @@ -207,7 +207,7 @@ dependencies { // androidx implementation "androidx.legacy:legacy-support-core-ui:1.0.0" - implementation "androidx.core:core-ktx:1.15.0" + implementation "androidx.core:core-ktx:1.16.0" implementation "androidx.activity:activity-ktx:1.10.1" implementation "androidx.fragment:fragment-ktx:1.8.6" @@ -233,7 +233,7 @@ dependencies { ksp "androidx.room:room-compiler:$room_version" // Compose - Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.03.01') + Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.04.01') implementation composeBom implementation 'androidx.compose.foundation:foundation' implementation "androidx.compose.ui:ui-android" diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt index 9001bf183b..34cc3a8616 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt @@ -15,27 +15,18 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Android import androidx.compose.material.icons.rounded.Bluetooth -import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DockedSearchBar import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection @@ -51,7 +42,8 @@ import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo -import io.github.sds100.keymapper.util.ui.compose.SimpleListItem +import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions +import io.github.sds100.keymapper.util.ui.compose.SimpleListItemFixedHeight import io.github.sds100.keymapper.util.ui.compose.SimpleListItemGroup import io.github.sds100.keymapper.util.ui.compose.SimpleListItemHeader import io.github.sds100.keymapper.util.ui.compose.SimpleListItemModel @@ -92,69 +84,18 @@ private fun ChooseActionScreen( onClickAction: (String) -> Unit = {}, onNavigateBack: () -> Unit = {}, ) { - var isExpanded: Boolean by rememberSaveable { mutableStateOf(false) } - Scaffold( modifier = modifier.displayCutoutPadding(), bottomBar = { BottomAppBar( modifier = Modifier.imePadding(), actions = { - IconButton(onClick = { - if (isExpanded) { - onCloseSearch() - isExpanded = false - } else { - onNavigateBack() - } - }) { - Icon( - Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = stringResource(R.string.bottom_app_bar_back_content_description), - ) - } - - DockedSearchBar( - modifier = Modifier.align(Alignment.CenterVertically), - inputField = { - SearchBarDefaults.InputField( - modifier = Modifier.align(Alignment.CenterVertically), - onSearch = { - onQueryChange(it) - isExpanded = false - }, - leadingIcon = { - Icon( - Icons.Rounded.Search, - contentDescription = null, - ) - }, - enabled = state is State.Data, - placeholder = { Text(stringResource(R.string.search_placeholder)) }, - query = query ?: "", - onQueryChange = onQueryChange, - expanded = isExpanded, - onExpandedChange = { expanded -> - if (expanded) { - isExpanded = true - } else { - onCloseSearch() - isExpanded = false - } - }, - ) - }, - // This is false to prevent an empty "content" showing underneath. - expanded = isExpanded, - onExpandedChange = { expanded -> - if (expanded) { - isExpanded = true - } else { - onCloseSearch() - isExpanded = false - } - }, - content = {}, + SearchAppBarActions( + onCloseSearch = onCloseSearch, + onNavigateBack = onNavigateBack, + onQueryChange = onQueryChange, + enabled = state is State.Data, + query = query, ) }, ) @@ -260,7 +201,7 @@ private fun ListScreen( group.items, contentType = { "list_item" }, ) { model -> - SimpleListItem( + SimpleListItemFixedHeight( modifier = Modifier.fillMaxWidth(), model = model, onClick = { onClickAction(model.id) }, @@ -363,6 +304,15 @@ private fun PreviewGrid() { isEnabled = false, ), + SimpleListItemModel( + "long", + title = "Very very very very very very very long title", + icon = ComposeIconInfo.Vector(Icons.Rounded.Bluetooth), + subtitle = null, + isSubtitleError = true, + isEnabled = false, + ), + ), ), diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt index 87f70c845d..ddde228691 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementFragment.kt @@ -4,6 +4,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.Fragment @@ -69,6 +71,7 @@ class InteractUiElementFragment : Fragment() { setContent { KeyMapperTheme { InteractUiElementScreen( + modifier = Modifier.fillMaxSize(), viewModel = viewModel, navigateBack = findNavController()::navigateUp, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 72b405b0ef..b6c94c3943 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.actions.uielement import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -46,14 +47,23 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.system.apps.ChooseAppScreen import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.ui.compose.icons.AdGroup import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons +import kotlinx.coroutines.flow.update + +private const val DEST_LANDING = "landing" +private const val DEST_SELECT_APP = "select_app" +private const val DEST_SELECT_ELEMENT = "select_element" @Composable fun InteractUiElementScreen( @@ -61,32 +71,70 @@ fun InteractUiElementScreen( viewModel: InteractUiElementViewModel, navigateBack: () -> Unit, ) { - val snackbarHostState = SnackbarHostState() + val navController = rememberNavController() + val recordState by viewModel.recordState.collectAsStateWithLifecycle() val selectedElementState by viewModel.selectedElementState.collectAsStateWithLifecycle() - InteractUiElementScreen( + val chooseAppState by viewModel.filteredAppListItems.collectAsStateWithLifecycle() + val appSearchQuery by viewModel.appSearchQuery.collectAsStateWithLifecycle() + + val onBackClick = { + if (!navController.navigateUp()) { + navigateBack() + } + } + + BackHandler(onBack = onBackClick) + + NavHost( modifier = modifier, - recordState = recordState, - selectedElementState = selectedElementState, - onBackClick = navigateBack, - onDoneClick = viewModel::onDoneClick, - onRecordClick = viewModel::onRecordClick, - snackbarHostState = snackbarHostState, - ) + navController = navController, + startDestination = DEST_LANDING, + enterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Left) }, + exitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + popEnterTransition = { slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + popExitTransition = { slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right) }, + ) { + composable(DEST_LANDING) { + LandingScreen( + modifier = Modifier.fillMaxSize(), + recordState = recordState, + selectedElementState = selectedElementState, + onRecordClick = viewModel::onRecordClick, + onBackClick = onBackClick, + onDoneClick = viewModel::onDoneClick, + openSelectAppScreen = { + navController.navigate(DEST_SELECT_APP) + }, + ) + } + + composable(DEST_SELECT_APP) { + ChooseAppScreen( + modifier = Modifier.fillMaxSize(), + title = stringResource(R.string.action_interact_ui_element_choose_element_title), + state = chooseAppState, + query = appSearchQuery, + onQueryChange = { query -> viewModel.appSearchQuery.update { query } }, + onCloseSearch = { viewModel.appSearchQuery.update { null } }, + onNavigateBack = onBackClick, + ) + } + } } @Composable -private fun InteractUiElementScreen( +private fun LandingScreen( modifier: Modifier = Modifier, recordState: State, selectedElementState: SelectedUiElementState?, + onRecordClick: () -> Unit = {}, onBackClick: () -> Unit = {}, onDoneClick: () -> Unit = {}, - onRecordClick: () -> Unit = {}, - snackbarHostState: SnackbarHostState = SnackbarHostState(), + openSelectAppScreen: () -> Unit = {}, ) { - BackHandler(onBack = onBackClick) + val snackbarHostState = SnackbarHostState() Scaffold( modifier.displayCutoutPadding(), @@ -126,7 +174,6 @@ private fun InteractUiElementScreen( start = startPadding, end = endPadding, ), - ) { Column { Text( @@ -156,6 +203,7 @@ private fun InteractUiElementScreen( .padding(horizontal = 16.dp), state = recordState, onRecordClick = onRecordClick, + openSelectAppScreen = openSelectAppScreen, ) Spacer(modifier = Modifier.height(8.dp)) @@ -173,6 +221,7 @@ private fun RecordingSection( modifier: Modifier = Modifier, state: State, onRecordClick: () -> Unit = {}, + openSelectAppScreen: () -> Unit = {}, ) { Column(modifier = modifier) { when (state) { @@ -186,7 +235,7 @@ private fun RecordingSection( InteractionCountBox( modifier = Modifier.fillMaxWidth(), interactionCount = interactionCount, - onClick = {}, + onClick = openSelectAppScreen, ) Spacer(modifier = Modifier.height(8.dp)) @@ -319,7 +368,7 @@ private fun SelectedElementSection(modifier: Modifier = Modifier, state: Selecte @Composable private fun PreviewEmpty() { KeyMapperTheme { - InteractUiElementScreen( + LandingScreen( recordState = State.Data(RecordUiElementState.Empty), selectedElementState = null, ) @@ -332,7 +381,7 @@ private fun PreviewSelectedElement() { val appIcon = LocalContext.current.drawable(R.mipmap.ic_launcher_round) KeyMapperTheme { - InteractUiElementScreen( + LandingScreen( recordState = State.Data(RecordUiElementState.Recorded(3)), selectedElementState = SelectedUiElementState( description = "Test", @@ -353,7 +402,7 @@ private fun PreviewSelectedElement() { @Composable private fun PreviewLoading() { KeyMapperTheme { - InteractUiElementScreen( + LandingScreen( recordState = State.Loading, selectedElementState = null, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 014731b87b..7f8a6de072 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -1,6 +1,8 @@ package io.github.sds100.keymapper.actions.uielement import android.view.accessibility.AccessibilityNodeInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Android import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -9,15 +11,22 @@ import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.Success +import io.github.sds100.keymapper.util.containsQuery import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.ifIsData +import io.github.sds100.keymapper.util.mapData import io.github.sds100.keymapper.util.onFailure +import io.github.sds100.keymapper.util.otherwise +import io.github.sds100.keymapper.util.then import io.github.sds100.keymapper.util.ui.PopupViewModel import io.github.sds100.keymapper.util.ui.PopupViewModelImpl import io.github.sds100.keymapper.util.ui.ResourceProvider import io.github.sds100.keymapper.util.ui.ViewModelHelper import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.SimpleListItemModel import io.github.sds100.keymapper.util.valueOrNull +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -25,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -72,6 +82,22 @@ class InteractUiElementViewModel( val selectedElementState: StateFlow = _selectedElementState.asStateFlow() + val appSearchQuery = MutableStateFlow(null) + + private val appListItems: Flow>> = useCase.interactedPackages + .map { state -> state.mapData(::createInteractedPackageListItems) } + + val filteredAppListItems = combine( + appListItems, + appSearchQuery, + ) { state, query -> + state.mapData { listItems -> + listItems.filter { model -> + model.title.containsQuery(query) + } + } + }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + fun loadAction(action: ActionData.InteractUiElement) { viewModelScope.launch { val appName = useCase.getAppName(action.packageName).valueOrNull() ?: action.packageName @@ -145,6 +171,23 @@ class InteractUiElementViewModel( } } + private fun createInteractedPackageListItems(packages: List): List { + return packages.map { packageName -> + val appName = useCase.getAppName(packageName).valueOrNull() ?: packageName + val appIcon = useCase + .getAppIcon(packageName) + .then { Success(ComposeIconInfo.Drawable(it)) } + .otherwise { Success(ComposeIconInfo.Vector(Icons.Rounded.Android)) } + .valueOrNull()!! + + SimpleListItemModel( + id = packageName, + title = appName, + icon = appIcon, + ) + } + } + private fun getNodeActionName(nodeAction: Int): String { return when (nodeAction) { AccessibilityNodeInfo.ACTION_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/ChooseAppScreen.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/ChooseAppScreen.kt new file mode 100644 index 0000000000..ca05260be4 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/ChooseAppScreen.kt @@ -0,0 +1,216 @@ +package io.github.sds100.keymapper.system.apps + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.drawable +import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions +import io.github.sds100.keymapper.util.ui.compose.SimpleListItem +import io.github.sds100.keymapper.util.ui.compose.SimpleListItemModel + +@Composable +fun ChooseAppScreen( + modifier: Modifier = Modifier, + title: String, + state: State>, + query: String? = null, + onQueryChange: (String) -> Unit = {}, + onCloseSearch: () -> Unit = {}, + onNavigateBack: () -> Unit = {}, + onClickApp: (String) -> Unit = {}, +) { + Scaffold( + modifier.displayCutoutPadding(), + bottomBar = { + BottomAppBar( + modifier = Modifier.imePadding(), + actions = { + SearchAppBarActions( + onCloseSearch = onCloseSearch, + onNavigateBack = onNavigateBack, + onQueryChange = onQueryChange, + enabled = state is State.Data, + query = query, + ) + }, + ) + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + Column { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp, + ), + text = title, + style = MaterialTheme.typography.titleLarge, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + when (state) { + State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) + is State.Data -> { + val items = state.data + + if (items.isEmpty()) { + EmptyScreen(modifier = Modifier.fillMaxSize()) + } else { + ListScreen( + modifier = Modifier.fillMaxSize(), + listItems = items, + onClick = onClickApp, + ) + } + } + } + } + } + } +} + +@Composable +private fun LoadingScreen(modifier: Modifier = Modifier) { + Box(modifier) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } +} + +@Composable +private fun EmptyScreen(modifier: Modifier = Modifier) { + Box(modifier) { + val shrug = stringResource(R.string.shrug) + val text = stringResource(R.string.app_list_empty) + Text( + modifier = Modifier.align(Alignment.Center), + text = buildAnnotatedString { + withStyle(MaterialTheme.typography.headlineLarge.toSpanStyle()) { + append(shrug) + } + appendLine() + appendLine() + withStyle(MaterialTheme.typography.bodyLarge.toSpanStyle()) { + append(text) + } + }, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun ListScreen( + modifier: Modifier = Modifier, + listItems: List, + onClick: (String) -> Unit, +) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(listItems) { model -> + SimpleListItem( + modifier = Modifier.fillMaxWidth(), + model = model, + onClick = { onClick(model.id) }, + ) + } + } +} + +@Preview +@Composable +private fun Empty() { + KeyMapperTheme { + ChooseAppScreen(title = "Choose app", state = State.Data(emptyList())) + } +} + +@Preview +@Composable +private fun Loading() { + KeyMapperTheme { + ChooseAppScreen(title = "Choose app", state = State.Loading) + } +} + +@Preview +@Composable +private fun Loaded() { + val icon = LocalContext.current.drawable(R.mipmap.ic_launcher_round) + + KeyMapperTheme { + ChooseAppScreen( + title = "Choose app", + state = State.Data( + listOf( + SimpleListItemModel( + id = "1", + title = "Key Mapper", + icon = ComposeIconInfo.Drawable(icon), + ), + SimpleListItemModel( + id = "2", + title = "Key Mapper", + icon = ComposeIconInfo.Drawable(icon), + ), + SimpleListItemModel( + id = "3", + title = "Key Mapper", + icon = ComposeIconInfo.Drawable(icon), + ), + ), + ), + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SearchAppBarActions.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SearchAppBarActions.kt new file mode 100644 index 0000000000..5ee078990d --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SearchAppBarActions.kt @@ -0,0 +1,90 @@ +package io.github.sds100.keymapper.util.ui.compose + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.DockedSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.github.sds100.keymapper.R + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun RowScope.SearchAppBarActions( + onCloseSearch: () -> Unit, + onNavigateBack: () -> Unit, + onQueryChange: (String) -> Unit, + enabled: Boolean, + query: String?, +) { + var isExpanded: Boolean by rememberSaveable { mutableStateOf(false) } + + IconButton(onClick = { + if (isExpanded) { + onCloseSearch() + isExpanded = false + } else { + onNavigateBack() + } + }) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.bottom_app_bar_back_content_description), + ) + } + + DockedSearchBar( + modifier = Modifier.Companion.align(Alignment.Companion.CenterVertically), + inputField = { + SearchBarDefaults.InputField( + modifier = Modifier.Companion.align(Alignment.Companion.CenterVertically), + onSearch = { + onQueryChange(it) + isExpanded = false + }, + leadingIcon = { + Icon( + Icons.Rounded.Search, + contentDescription = null, + ) + }, + enabled = enabled, + placeholder = { Text(stringResource(R.string.search_placeholder)) }, + query = query ?: "", + onQueryChange = onQueryChange, + expanded = isExpanded, + onExpandedChange = { expanded -> + if (expanded) { + isExpanded = true + } else { + onCloseSearch() + isExpanded = false + } + }, + ) + }, + // This is false to prevent an empty "content" showing underneath. + expanded = isExpanded, + onExpandedChange = { expanded -> + if (expanded) { + isExpanded = true + } else { + onCloseSearch() + isExpanded = false + } + }, + content = {}, + ) +} diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SimpleListItem.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SimpleListItem.kt index d3a31360bc..0c758c99e7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SimpleListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/SimpleListItem.kt @@ -1,11 +1,14 @@ package io.github.sds100.keymapper.util.ui.compose +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -13,11 +16,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -46,58 +51,86 @@ fun SimpleListItemHeader( } @Composable -fun SimpleListItem( +fun SimpleListItemFixedHeight( modifier: Modifier = Modifier, model: SimpleListItemModel, onClick: () -> Unit = {}, ) { - OutlinedCard(modifier = modifier.height(48.dp), onClick = onClick, enabled = model.isEnabled) { - Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { - Spacer(modifier = Modifier.width(16.dp)) + SimpleListItem( + modifier = modifier.height(56.dp), + model = model, + onClick = onClick, + ) +} - when (model.icon) { - is ComposeIconInfo.Vector -> Icon( - modifier = Modifier.size(26.dp), - imageVector = model.icon.imageVector, - contentDescription = null, - tint = LocalContentColor.current, - ) +@Composable +fun SimpleListItem( + modifier: Modifier = Modifier, + model: SimpleListItemModel, + onClick: () -> Unit = {}, +) { + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides 16.dp, + ) { + OutlinedCard( + modifier = modifier.height(IntrinsicSize.Min), + onClick = onClick, + enabled = model.isEnabled, + ) { + Row( + modifier = Modifier + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(16.dp)) - is ComposeIconInfo.Drawable -> { - val painter = rememberDrawablePainter(model.icon.drawable) - Icon( + when (model.icon) { + is ComposeIconInfo.Vector -> Icon( modifier = Modifier.size(26.dp), - painter = painter, + imageVector = model.icon.imageVector, contentDescription = null, - tint = Color.Unspecified, + tint = LocalContentColor.current, ) - } - } - Spacer(modifier = Modifier.width(16.dp)) + is ComposeIconInfo.Drawable -> { + val painter = rememberDrawablePainter(model.icon.drawable) + Icon( + modifier = Modifier.size(26.dp), + painter = painter, + contentDescription = null, + tint = Color.Unspecified, + ) + } + } - Column( - modifier = Modifier.padding(end = 16.dp), - ) { - Text( - text = model.title, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (model.subtitle == null) { - 2 - } else { - 1 - }, - overflow = TextOverflow.Ellipsis, - ) + Spacer(modifier = Modifier.width(16.dp)) - if (model.subtitle != null) { + Column( + modifier = Modifier + .padding(end = 16.dp) + .heightIn(min = 36.dp), + verticalArrangement = Arrangement.Center, + ) { Text( - text = model.subtitle, - color = if (model.isSubtitleError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, + text = model.title, + style = MaterialTheme.typography.bodyMedium, + maxLines = if (model.subtitle == null) { + 2 + } else { + 1 + }, overflow = TextOverflow.Ellipsis, ) + + if (model.subtitle != null) { + Text( + text = model.subtitle, + color = if (model.isSubtitleError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index beb6c3c235..915b5572fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ Enable accessibility service Restart accessibility service Share - + Nothing here! Stop repeating when… Trigger is released Trigger is pressed again From 410c1ae0f7c81a7735d9984c8f4aa3cc28f64ebb Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 13:33:46 +0200 Subject: [PATCH 15/69] #257 listing recorded UI elements works --- .../sds100/keymapper/actions/ActionData.kt | 5 +- .../actions/ActionDataEntityMapper.kt | 22 +- .../actions/PerformActionsUseCase.kt | 2 +- .../uielement/ChooseUiElementScreen.kt | 325 ++++++++++++++++++ .../uielement/InteractUiElementScreen.kt | 30 +- .../uielement/InteractUiElementUseCase.kt | 5 + .../uielement/InteractUiElementViewModel.kt | 151 ++++++-- .../actions/uielement/NodeInteractionType.kt | 14 + .../data/entities/AccessibilityNodeEntity.kt | 5 +- .../AccessibilityNodeRepository.kt | 24 +- .../AccessibilityNodeRecorder.kt | 14 +- app/src/main/res/values/strings.xml | 3 +- 12 files changed, 539 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index f2d4e8faa1..9b36222391 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType +import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.intents.IntentExtraModel @@ -849,7 +850,7 @@ sealed class ActionData : Comparable { @Serializable data class InteractUiElement( val description: String, - val nodeAction: Int, + val nodeAction: NodeInteractionType, val packageName: String, val text: String?, val contentDescription: String?, @@ -859,7 +860,7 @@ sealed class ActionData : Comparable { /** * A list of the allowed accessibility node actions. */ - val nodeActions: List, + val nodeActions: List, ) : ActionData() { override val id: ActionId = ActionId.INTERACT_UI_ELEMENT } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index bcb0d4f8fc..77f6b92d87 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType +import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.db.typeconverter.ConstantTypeConverters import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.data.entities.EntityExtra @@ -12,6 +13,8 @@ import io.github.sds100.keymapper.system.network.HttpMethod import io.github.sds100.keymapper.system.volume.DndMode import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeStream +import io.github.sds100.keymapper.util.Error +import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success import io.github.sds100.keymapper.util.getKey import io.github.sds100.keymapper.util.success @@ -269,7 +272,7 @@ object ActionDataEntityMapper { ActionId.VOLUME_TOGGLE_MUTE, ActionId.VOLUME_UNMUTE, ActionId.VOLUME_MUTE, - -> { + -> { val showVolumeUi = entity.flags.hasFlag(ActionEntity.ACTION_FLAG_SHOW_VOLUME_UI) @@ -549,14 +552,17 @@ object ActionDataEntityMapper { entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID).valueOrNull() val actions = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS).then { - val intList = it.split(",").map { action -> action.toInt() } - Success(intList) + val typeList = it + .split(",") + .mapNotNull { action -> convertNodeInteractionType(it).valueOrNull() } + + Success(typeList) }.valueOrNull() ?: emptyList() val nodeAction = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_NODE_ACTION).then { - Success(it.toInt()) - }.valueOrNull()!! + convertNodeInteractionType(it) + }.valueOrNull() ?: return null ActionData.InteractUiElement( description = entity.data, @@ -573,6 +579,12 @@ object ActionDataEntityMapper { } } + private fun convertNodeInteractionType(string: String): Result = try { + Success(NodeInteractionType.valueOf(string)) + } catch (e: IllegalArgumentException) { + Error.Exception(e) + } + fun toEntity(data: ActionData): ActionEntity { val type = when (data) { is ActionData.Intent -> ActionEntity.Type.INTENT diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 3418b6edbc..0bb4dc9a59 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -811,7 +811,7 @@ class PerformActionsUseCaseImpl( // TODO compare other values node.uniqueId == action.uniqueId }, - performAction = { AccessibilityNodeAction(action = action.nodeAction) }, + performAction = { AccessibilityNodeAction(action = action.nodeAction.accessibilityActionId) }, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt new file mode 100644 index 0000000000..d9a111ffc9 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -0,0 +1,325 @@ +package io.github.sds100.keymapper.actions.uielement + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions + +@Composable +fun ChooseElementScreen( + modifier: Modifier = Modifier, + state: State>, + query: String?, + onCloseSearch: () -> Unit = {}, + onNavigateBack: () -> Unit = {}, + onQueryChange: (String) -> Unit = {}, + onClickElement: (Long) -> Unit = {}, +) { + Scaffold( + modifier.displayCutoutPadding(), + bottomBar = { + BottomAppBar( + modifier = Modifier.imePadding(), + actions = { + SearchAppBarActions( + onCloseSearch = onCloseSearch, + onNavigateBack = onNavigateBack, + onQueryChange = onQueryChange, + enabled = state is State.Data, + query = query, + ) + }, + ) + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + Column { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp, + ), + text = stringResource(R.string.action_interact_ui_element_choose_element_title), + style = MaterialTheme.typography.titleLarge, + ) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.ErrorOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_subtitle), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleSmall, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), + style = MaterialTheme.typography.bodyMedium, + ) + + // TODO key mapper dropdown menu + + when (state) { + State.Loading -> LoadingList(modifier = Modifier.fillMaxSize()) + is State.Data -> { + val items = state.data + + if (items.isEmpty()) { + EmptyList(modifier = Modifier.fillMaxSize()) + } else { + LoadedList( + modifier = Modifier.fillMaxSize(), + listItems = items, + onClick = onClickElement, + ) + } + } + } + } + } + } +} + +@Composable +private fun LoadingList(modifier: Modifier = Modifier) { + Box(modifier) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } +} + +@Composable +private fun EmptyList(modifier: Modifier = Modifier) { + Box(modifier) { + val shrug = stringResource(R.string.shrug) + val text = stringResource(R.string.ui_element_list_empty) + Text( + modifier = Modifier.align(Alignment.Center), + text = buildAnnotatedString { + withStyle(MaterialTheme.typography.headlineLarge.toSpanStyle()) { + append(shrug) + } + appendLine() + appendLine() + withStyle(MaterialTheme.typography.bodyLarge.toSpanStyle()) { + append(text) + } + }, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun LoadedList( + modifier: Modifier = Modifier, + listItems: List, + onClick: (Long) -> Unit, +) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(listItems, key = { it.id }) { model -> + UiElementListItem( + modifier = Modifier.fillMaxWidth(), + model = model, + onClick = { onClick(model.id) }, + ) + } + } +} + +@Composable +private fun UiElementListItem( + modifier: Modifier = Modifier, + model: UiElementListItemModel, + onClick: () -> Unit, +) { + OutlinedCard(modifier = modifier, onClick = onClick) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (model.nodeViewResourceId != null) { + Text( + text = model.nodeViewResourceId, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + if (model.nodeText != null) { + Text( + text = model.nodeText, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + if (model.nodeClassName != null) { + TextWithLeadingLabel( + title = stringResource(R.string.action_interact_ui_element_class_name_label), + text = model.nodeClassName, + ) + } + + if (model.nodeUniqueId != null) { + TextWithLeadingLabel( + title = stringResource(R.string.action_interact_ui_element_unique_id_label), + text = model.nodeUniqueId, + ) + } + + TextWithLeadingLabel( + title = stringResource(R.string.action_interact_ui_element_interaction_types_label), + text = model.interactionTypes, + ) + } + } +} + +@Composable +private fun TextWithLeadingLabel( + modifier: Modifier = Modifier, + title: String, + text: String, +) { + val text = buildAnnotatedString { + pushStyle( + MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold).toSpanStyle(), + ) + append(title) + pop() + append(": ") + append(text) + } + + Text( + modifier = modifier, + text = text, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) +} + +@Preview +@Composable +private fun Empty() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(emptyList()), + query = "Key Mapper", + ) + } +} + +@Preview +@Composable +private fun Loading() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Loading, + query = null, + ) + } +} + +@Preview +@Composable +private fun Loaded() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data( + listOf( + UiElementListItemModel( + id = 1L, + nodeText = "Open Settings", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "menu_button", + nodeUniqueId = "123456789", + interactionTypes = "Tap, Tap and hold, Scroll forward", + ), + ), + ), + query = "Key Mapper", + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index b6c94c3943..cac5489f21 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -76,9 +76,12 @@ fun InteractUiElementScreen( val recordState by viewModel.recordState.collectAsStateWithLifecycle() val selectedElementState by viewModel.selectedElementState.collectAsStateWithLifecycle() - val chooseAppState by viewModel.filteredAppListItems.collectAsStateWithLifecycle() + val appListState by viewModel.filteredAppListItems.collectAsStateWithLifecycle() val appSearchQuery by viewModel.appSearchQuery.collectAsStateWithLifecycle() + val elementListState by viewModel.filteredElementListItems.collectAsStateWithLifecycle() + val elementSearchQuery by viewModel.elementSearchQuery.collectAsStateWithLifecycle() + val onBackClick = { if (!navController.navigateUp()) { navigateBack() @@ -114,11 +117,30 @@ fun InteractUiElementScreen( ChooseAppScreen( modifier = Modifier.fillMaxSize(), title = stringResource(R.string.action_interact_ui_element_choose_element_title), - state = chooseAppState, + state = appListState, query = appSearchQuery, onQueryChange = { query -> viewModel.appSearchQuery.update { query } }, onCloseSearch = { viewModel.appSearchQuery.update { null } }, onNavigateBack = onBackClick, + onClickApp = { + viewModel.onSelectApp(it) + navController.navigate(DEST_SELECT_ELEMENT) + }, + ) + } + + composable(DEST_SELECT_ELEMENT) { + ChooseElementScreen( + modifier = Modifier.fillMaxSize(), + state = elementListState, + query = elementSearchQuery, + onCloseSearch = { viewModel.elementSearchQuery.update { null } }, + onNavigateBack = onBackClick, + onQueryChange = { query -> viewModel.elementSearchQuery.update { query } }, + onClickElement = { + viewModel.onSelectElement(it) + navController.popBackStack(route = DEST_LANDING, inclusive = false) + }, ) } } @@ -388,11 +410,11 @@ private fun PreviewSelectedElement() { appName = "Test App", appIcon = ComposeIconInfo.Drawable(appIcon), nodeText = "Test Node", - nodeClassName = "Test Class", + nodeClassName = "android.widget.ImageButton", nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", nodeUniqueId = "123", interactionTypes = listOf(), - selectedInteraction = 0, + selectedInteraction = NodeInteractionType.LONG_CLICK, ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt index 79688b1629..c1553c6291 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt @@ -54,6 +54,10 @@ class InteractUiElementController( } } + override suspend fun getInteractionById(id: Long): AccessibilityNodeEntity? { + return nodeRepository.get(id) + } + override fun getAppName(packageName: String): Result = packageManagerAdapter.getAppName(packageName) override fun getAppIcon(packageName: String): Result = packageManagerAdapter.getAppIcon(packageName) @@ -79,6 +83,7 @@ interface InteractUiElementUseCase { val interactionCount: Flow> val interactedPackages: Flow>> fun getInteractionsByPackage(packageName: String): Flow>> + suspend fun getInteractionById(id: Long): AccessibilityNodeEntity? fun getAppName(packageName: String): Result fun getAppIcon(packageName: String): Result diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 7f8a6de072..d16db3acf6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.actions.uielement -import android.view.accessibility.AccessibilityNodeInfo import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Android import androidx.lifecycle.ViewModel @@ -8,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.R import io.github.sds100.keymapper.actions.ActionData +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.system.accessibility.RecordAccessibilityNodeState import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.State @@ -26,6 +26,7 @@ import io.github.sds100.keymapper.util.ui.ViewModelHelper import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.ui.compose.SimpleListItemModel import io.github.sds100.keymapper.util.valueOrNull +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -34,6 +35,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -85,7 +88,7 @@ class InteractUiElementViewModel( val appSearchQuery = MutableStateFlow(null) private val appListItems: Flow>> = useCase.interactedPackages - .map { state -> state.mapData(::createInteractedPackageListItems) } + .map { state -> state.mapData { list -> list.map(::buildInteractedPackageListItem) } } val filteredAppListItems = combine( appListItems, @@ -98,11 +101,39 @@ class InteractUiElementViewModel( } }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + private val selectedApp = MutableStateFlow(null) + + val elementSearchQuery = MutableStateFlow(null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val elementListItems: StateFlow>> = selectedApp + .filterNotNull() + .flatMapLatest { packageName -> useCase.getInteractionsByPackage(packageName) } + .map { state -> state.mapData { list -> list.map(::buildUiElementListItem) } } + .stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + + val filteredElementListItems = combine( + elementListItems, + elementSearchQuery, + ) { state, query -> + state.mapData { listItems -> + listItems.filter { model -> + val modelString = buildString { + append(model.nodeText) + append(" ") + append(model.nodeClassName) + append(" ") + append(model.nodeViewResourceId) + } + modelString.containsQuery(query) + } + } + }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + fun loadAction(action: ActionData.InteractUiElement) { viewModelScope.launch { val appName = useCase.getAppName(action.packageName).valueOrNull() ?: action.packageName - val appIcon = useCase.getAppIcon(action.packageName).valueOrNull() - ?.let { ComposeIconInfo.Drawable(it) } + val appIcon = getAppIcon(action.packageName) val newState = SelectedUiElementState( description = action.description, @@ -153,6 +184,36 @@ class InteractUiElementViewModel( } } + fun onSelectApp(packageName: String) { + elementSearchQuery.update { null } + selectedApp.update { packageName } + } + + fun onSelectElement(id: Long) { + viewModelScope.launch { + val interaction = useCase.getInteractionById(id) ?: return@launch + + val appName = + useCase.getAppName(interaction.packageName).valueOrNull() ?: interaction.packageName + val appIcon = getAppIcon(interaction.packageName) + + val newState = SelectedUiElementState( + description = "", + appName = appName, + appIcon = appIcon, + nodeText = interaction.text ?: interaction.contentDescription, + nodeClassName = interaction.className, + nodeViewResourceId = interaction.viewResourceId, + nodeUniqueId = interaction.uniqueId, + interactionTypes = interaction.actions, + selectedInteraction = interaction.userInteractedActionId + ?: interaction.actions.first(), + ) + + _selectedElementState.update { newState } + } + } + private suspend fun startRecording() { useCase.startRecording().onFailure { error -> if (error == Error.AccessibilityServiceDisabled) { @@ -171,37 +232,46 @@ class InteractUiElementViewModel( } } - private fun createInteractedPackageListItems(packages: List): List { - return packages.map { packageName -> - val appName = useCase.getAppName(packageName).valueOrNull() ?: packageName - val appIcon = useCase - .getAppIcon(packageName) - .then { Success(ComposeIconInfo.Drawable(it)) } - .otherwise { Success(ComposeIconInfo.Vector(Icons.Rounded.Android)) } - .valueOrNull()!! - - SimpleListItemModel( - id = packageName, - title = appName, - icon = appIcon, - ) - } + private fun buildInteractedPackageListItem(packageName: String): SimpleListItemModel { + val appName = useCase.getAppName(packageName).valueOrNull() ?: packageName + val appIcon = getAppIcon(packageName) + + return SimpleListItemModel( + id = packageName, + title = appName, + icon = appIcon, + ) } - private fun getNodeActionName(nodeAction: Int): String { - return when (nodeAction) { - AccessibilityNodeInfo.ACTION_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) - AccessibilityNodeInfo.ACTION_LONG_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_long_click) - AccessibilityNodeInfo.ACTION_FOCUS -> getString(R.string.action_interact_ui_element_interaction_type_focus) - AccessibilityNodeInfo.ACTION_SELECT -> getString(R.string.action_interact_ui_element_interaction_type_select) - AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_forward) - AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_backward) - AccessibilityNodeInfo.ACTION_EXPAND -> getString(R.string.action_interact_ui_element_interaction_type_expand) - AccessibilityNodeInfo.ACTION_COLLAPSE -> getString(R.string.action_interact_ui_element_interaction_type_collapse) - else -> getString( - R.string.action_interact_ui_element_interaction_type_unknown, - nodeAction, - ) + private fun buildUiElementListItem(node: AccessibilityNodeEntity): UiElementListItemModel { + val resourceIdText = node.viewResourceId?.split("/")?.lastOrNull() + + return UiElementListItemModel( + id = node.id, + nodeViewResourceId = resourceIdText, + nodeText = node.text ?: node.contentDescription, + nodeClassName = node.className, + nodeUniqueId = node.uniqueId?.toString(), + interactionTypes = node.actions.joinToString { getInteractionTypeString(it) }, + ) + } + + private fun getAppIcon(packageName: String): ComposeIconInfo = useCase + .getAppIcon(packageName) + .then { Success(ComposeIconInfo.Drawable(it)) } + .otherwise { Success(ComposeIconInfo.Vector(Icons.Rounded.Android)) } + .valueOrNull()!! + + private fun getInteractionTypeString(interactionType: NodeInteractionType): String { + return when (interactionType) { + NodeInteractionType.CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) + NodeInteractionType.LONG_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_long_click) + NodeInteractionType.FOCUS -> getString(R.string.action_interact_ui_element_interaction_type_focus) + NodeInteractionType.SELECT -> getString(R.string.action_interact_ui_element_interaction_type_select) + NodeInteractionType.SCROLL_FORWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_forward) + NodeInteractionType.SCROLL_BACKWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_backward) + NodeInteractionType.EXPAND -> getString(R.string.action_interact_ui_element_interaction_type_expand) + NodeInteractionType.COLLAPSE -> getString(R.string.action_interact_ui_element_interaction_type_collapse) } } @@ -219,13 +289,13 @@ class InteractUiElementViewModel( data class SelectedUiElementState( val description: String, val appName: String, - val appIcon: ComposeIconInfo.Drawable?, + val appIcon: ComposeIconInfo, val nodeText: String?, val nodeClassName: String?, val nodeViewResourceId: String?, val nodeUniqueId: String?, - val interactionTypes: List, - val selectedInteraction: Int, + val interactionTypes: List, + val selectedInteraction: NodeInteractionType, ) sealed class RecordUiElementState { @@ -238,3 +308,12 @@ sealed class RecordUiElementState { data object Empty : RecordUiElementState() } + +data class UiElementListItemModel( + val id: Long, + val nodeViewResourceId: String?, + val nodeText: String?, + val nodeClassName: String?, + val nodeUniqueId: String?, + val interactionTypes: String, +) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt new file mode 100644 index 0000000000..f31a6520f5 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt @@ -0,0 +1,14 @@ +package io.github.sds100.keymapper.actions.uielement + +import android.view.accessibility.AccessibilityNodeInfo + +enum class NodeInteractionType(val accessibilityActionId: Int) { + CLICK(AccessibilityNodeInfo.ACTION_CLICK), + LONG_CLICK(AccessibilityNodeInfo.ACTION_LONG_CLICK), + FOCUS(AccessibilityNodeInfo.ACTION_FOCUS), + SELECT(AccessibilityNodeInfo.ACTION_SELECT), + SCROLL_FORWARD(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD), + SCROLL_BACKWARD(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD), + EXPAND(AccessibilityNodeInfo.ACTION_EXPAND), + COLLAPSE(AccessibilityNodeInfo.ACTION_COLLAPSE), +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index 8792942277..0913213f06 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.data.entities import android.os.Parcelable +import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -18,11 +19,11 @@ data class AccessibilityNodeEntity( /** * A list of the allowed accessibility node actions. */ - val actions: List, + val actions: List, /** * The accessibility action id of how the user interacted * with this node. This is null if the user didn't interact with * this node. */ - val userInteractedActionId: Int?, + val userInteractedActionId: NodeInteractionType?, ) : Parcelable diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt index be21fa91a6..ee0df25555 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt @@ -2,31 +2,41 @@ package io.github.sds100.keymapper.data.repositories import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.dataOrNull +import io.github.sds100.keymapper.util.mapData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch interface AccessibilityNodeRepository { val nodes: Flow>> - fun insert(vararg node: AccessibilityNodeEntity) + fun get(id: Long): AccessibilityNodeEntity? + fun insert(node: AccessibilityNodeEntity) fun deleteAll() } class AccessibilityNodeRepositoryImpl(private val coroutineScope: CoroutineScope) : AccessibilityNodeRepository { - // TODO have a DAO to remember between app launches and so it isn't all cached in memory? + // TODO have a DAO to remember between app launches and so it isn't all cached in memory - also handles IDs automatically. + // TODO do not insert duplicates where all fields are the same override val nodes = MutableStateFlow>>(State.Data(ArrayList(128))) - override fun insert(vararg node: AccessibilityNodeEntity) { - coroutineScope.launch { - val currentState = nodes.value - if (currentState is State.Data) { - nodes.emit(State.Data(currentState.data.plus(node))) + override fun insert(node: AccessibilityNodeEntity) { + nodes.update { currentState -> + currentState.mapData { list -> + val nodeWithId = node.copy(id = list.size + 1L) + list.plus(nodeWithId) } } } + override fun get(id: Long): AccessibilityNodeEntity? { + val nodes = nodes.value.dataOrNull() ?: return null + return nodes.find { it.id == id } + } + override fun deleteAll() { coroutineScope.launch { nodes.emit(State.Data(ArrayList(128))) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index d031696e04..678f3ec536 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.system.accessibility import android.os.Build import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent +import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository import kotlinx.coroutines.flow.MutableStateFlow @@ -59,10 +60,17 @@ class AccessibilityNodeRecorder( val source = event.source ?: return - if (source.actionList.isNullOrEmpty()) { + val interactionTypes = source.actionList.mapNotNull { action -> + NodeInteractionType.entries.find { it.accessibilityActionId == action.id } + }.distinct() + + if (interactionTypes.isEmpty()) { return } + val userInteractedActionId = + NodeInteractionType.entries.find { it.accessibilityActionId == event.action } + val entity = AccessibilityNodeEntity( packageName = event.packageName.toString(), @@ -75,8 +83,8 @@ class AccessibilityNodeRecorder( } else { null }, - actions = source.actionList?.map { it.id } ?: emptyList(), - userInteractedActionId = event.action, + actions = interactionTypes, + userInteractedActionId = userInteractedActionId, ) nodeRepository.insert(entity) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 915b5572fb..7fc86c0576 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Restart accessibility service Share Nothing here! + Nothing here! Stop repeating when… Trigger is released Trigger is pressed again @@ -1131,9 +1132,9 @@ Text - View ID Class name Unique ID + Interaction types From 6824ed356bdf1f35669aa782e28a292ab8074f54 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 15:40:21 +0200 Subject: [PATCH 16/69] #257 filter interaction types --- .../actions/HttpRequestBottomSheet.kt | 10 +-- .../uielement/ChooseUiElementScreen.kt | 86 ++++++++++++++----- .../uielement/InteractUiElementScreen.kt | 3 +- .../uielement/InteractUiElementViewModel.kt | 70 +++++++++++++-- .../data/entities/AccessibilityNodeEntity.kt | 2 +- .../AccessibilityNodeRecorder.kt | 2 +- .../util/ui/compose/KeyMapperDropdownMenu.kt | 26 +++--- app/src/main/res/values/strings.xml | 1 + 8 files changed, 150 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt index 25725b2a41..ee00632d66 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt @@ -48,7 +48,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @OptIn(ExperimentalMaterial3Api::class) @Composable fun HttpRequestBottomSheet(delegate: CreateActionDelegate) { - val scope = rememberCoroutineScope() + rememberCoroutineScope() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) if (delegate.httpRequestBottomSheetState != null) { @@ -139,10 +139,10 @@ private fun HttpRequestBottomSheet( .padding(horizontal = 16.dp), expanded = methodExpanded, onExpandedChange = { methodExpanded = it }, - value = state.method.toString(), - onValueChanged = { - onSelectMethod(HttpMethod.valueOf(it)) - }, + label = stringResource(R.string.action_http_request_method_label), + selectedValue = state.method, + values = HttpMethod.entries.map { it to it.toString() }, + onValueChanged = onSelectMethod, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index d9a111ffc9..3d3355d28b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -28,6 +28,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection @@ -42,18 +46,22 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions @Composable fun ChooseElementScreen( modifier: Modifier = Modifier, - state: State>, + state: State, query: String?, onCloseSearch: () -> Unit = {}, onNavigateBack: () -> Unit = {}, onQueryChange: (String) -> Unit = {}, onClickElement: (Long) -> Unit = {}, + onSelectInteractionType: (NodeInteractionType?) -> Unit = {}, ) { + var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + Scaffold( modifier.displayCutoutPadding(), bottomBar = { @@ -129,20 +137,31 @@ fun ChooseElementScreen( text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), style = MaterialTheme.typography.bodyMedium, ) - - // TODO key mapper dropdown menu + Spacer(modifier = Modifier.height(8.dp)) when (state) { State.Loading -> LoadingList(modifier = Modifier.fillMaxSize()) is State.Data -> { - val items = state.data + val listItems = state.data.listItems - if (items.isEmpty()) { + if (listItems.isEmpty()) { EmptyList(modifier = Modifier.fillMaxSize()) } else { + KeyMapperDropdownMenu( + modifier = Modifier.padding(horizontal = 16.dp), + expanded = interactionTypeExpanded, + onExpandedChange = { interactionTypeExpanded = it }, + label = stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown), + values = state.data.interactionTypes, + selectedValue = state.data.selectedInteractionType, + onValueChanged = onSelectInteractionType, + ) + + Spacer(modifier = Modifier.height(8.dp)) + LoadedList( modifier = Modifier.fillMaxSize(), - listItems = items, + listItems = listItems, onClick = onClickElement, ) } @@ -216,7 +235,7 @@ private fun UiElementListItem( ) { if (model.nodeViewResourceId != null) { Text( - text = model.nodeViewResourceId, + text = "View ID: ${model.nodeViewResourceId}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, maxLines = 2, @@ -226,7 +245,7 @@ private fun UiElementListItem( if (model.nodeText != null) { Text( - text = model.nodeText, + text = "\"${model.nodeText}\"", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -249,7 +268,7 @@ private fun UiElementListItem( TextWithLeadingLabel( title = stringResource(R.string.action_interact_ui_element_interaction_types_label), - text = model.interactionTypes, + text = model.interactionTypesText, ) } } @@ -263,7 +282,7 @@ private fun TextWithLeadingLabel( ) { val text = buildAnnotatedString { pushStyle( - MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold).toSpanStyle(), + MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold).toSpanStyle(), ) append(title) pop() @@ -285,7 +304,13 @@ private fun TextWithLeadingLabel( private fun Empty() { KeyMapperTheme { ChooseElementScreen( - state = State.Data(emptyList()), + state = State.Data( + SelectUiElementState( + listItems = emptyList(), + interactionTypes = emptyList(), + selectedInteractionType = null, + ), + ), query = "Key Mapper", ) } @@ -305,20 +330,35 @@ private fun Loading() { @Preview @Composable private fun Loaded() { + val listItems = listOf( + UiElementListItemModel( + id = 1L, + nodeText = "Open Settings", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "menu_button", + nodeUniqueId = "123456789", + interactionTypesText = "Tap, Tap and hold, Scroll forward", + interactionTypes = setOf( + NodeInteractionType.CLICK, + NodeInteractionType.LONG_CLICK, + NodeInteractionType.SCROLL_FORWARD, + ), + ), + ) + + val state = SelectUiElementState( + listItems = listItems, + interactionTypes = listOf( + null to "Any", + NodeInteractionType.CLICK to "Tap", + NodeInteractionType.LONG_CLICK to "Tap and hold", + ), + selectedInteractionType = null, + ) + KeyMapperTheme { ChooseElementScreen( - state = State.Data( - listOf( - UiElementListItemModel( - id = 1L, - nodeText = "Open Settings", - nodeClassName = "android.widget.ImageButton", - nodeViewResourceId = "menu_button", - nodeUniqueId = "123456789", - interactionTypes = "Tap, Tap and hold, Scroll forward", - ), - ), - ), + state = State.Data(state), query = "Key Mapper", ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index cac5489f21..6d9b6164af 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -79,7 +79,7 @@ fun InteractUiElementScreen( val appListState by viewModel.filteredAppListItems.collectAsStateWithLifecycle() val appSearchQuery by viewModel.appSearchQuery.collectAsStateWithLifecycle() - val elementListState by viewModel.filteredElementListItems.collectAsStateWithLifecycle() + val elementListState by viewModel.selectUiElementState.collectAsStateWithLifecycle() val elementSearchQuery by viewModel.elementSearchQuery.collectAsStateWithLifecycle() val onBackClick = { @@ -141,6 +141,7 @@ fun InteractUiElementScreen( viewModel.onSelectElement(it) navController.popBackStack(route = DEST_LANDING, inclusive = false) }, + onSelectInteractionType = viewModel::onSelectInteractionTypeFilter, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index d16db3acf6..23c995622a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -106,18 +106,31 @@ class InteractUiElementViewModel( val elementSearchQuery = MutableStateFlow(null) @OptIn(ExperimentalCoroutinesApi::class) - private val elementListItems: StateFlow>> = selectedApp + private val interactionsByPackage: StateFlow>> = selectedApp .filterNotNull() .flatMapLatest { packageName -> useCase.getInteractionsByPackage(packageName) } - .map { state -> state.mapData { list -> list.map(::buildUiElementListItem) } } .stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) - val filteredElementListItems = combine( + private val elementListItems: Flow>> = interactionsByPackage + .map { state -> state.mapData { list -> list.map(::buildUiElementListItem) } } + + private val interactionTypesFilterItems: Flow>>> = + interactionsByPackage + .map { state -> state.mapData(::buildInteractionTypeFilterItems) } + + private val selectedInteractionTypeFilter = MutableStateFlow(null) + + private val filteredElementListItems = combine( elementListItems, elementSearchQuery, - ) { state, query -> + selectedInteractionTypeFilter, + ) { state, query, interactionType -> state.mapData { listItems -> listItems.filter { model -> + if (interactionType != null && !model.interactionTypes.contains(interactionType)) { + return@filter false + } + val modelString = buildString { append(model.nodeText) append(" ") @@ -130,6 +143,22 @@ class InteractUiElementViewModel( } }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + val selectUiElementState: StateFlow> = combine( + filteredElementListItems, + interactionTypesFilterItems, + selectedInteractionTypeFilter, + ) { listItemsState, interactionTypesState, selectedInteractionType -> + val listItems = listItemsState.dataOrNull() ?: return@combine State.Loading + val interactionTypes = interactionTypesState.dataOrNull() ?: return@combine State.Loading + + val newState = SelectUiElementState( + listItems = listItems, + interactionTypes = interactionTypes, + selectedInteractionType = selectedInteractionType, + ) + State.Data(newState) + }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + fun loadAction(action: ActionData.InteractUiElement) { viewModelScope.launch { val appName = useCase.getAppName(action.packageName).valueOrNull() ?: action.packageName @@ -205,7 +234,7 @@ class InteractUiElementViewModel( nodeClassName = interaction.className, nodeViewResourceId = interaction.viewResourceId, nodeUniqueId = interaction.uniqueId, - interactionTypes = interaction.actions, + interactionTypes = interaction.actions.toList(), selectedInteraction = interaction.userInteractedActionId ?: interaction.actions.first(), ) @@ -214,6 +243,10 @@ class InteractUiElementViewModel( } } + fun onSelectInteractionTypeFilter(interactionType: NodeInteractionType?) { + selectedInteractionTypeFilter.update { interactionType } + } + private suspend fun startRecording() { useCase.startRecording().onFailure { error -> if (error == Error.AccessibilityServiceDisabled) { @@ -252,10 +285,26 @@ class InteractUiElementViewModel( nodeText = node.text ?: node.contentDescription, nodeClassName = node.className, nodeUniqueId = node.uniqueId?.toString(), - interactionTypes = node.actions.joinToString { getInteractionTypeString(it) }, + interactionTypesText = node.actions.joinToString { getInteractionTypeString(it) }, + interactionTypes = node.actions, ) } + private fun buildInteractionTypeFilterItems(nodes: List): List> { + val interactionTypes = nodes.flatMap { it.actions }.toSet() + + return buildList { + add(null to getString(R.string.action_interact_ui_element_interaction_type_any)) + + // They should always be in the same order so iterate over the Enum entries. + for (type in NodeInteractionType.entries) { + if (interactionTypes.contains(type)) { + add(type to getInteractionTypeString(type)) + } + } + } + } + private fun getAppIcon(packageName: String): ComposeIconInfo = useCase .getAppIcon(packageName) .then { Success(ComposeIconInfo.Drawable(it)) } @@ -309,11 +358,18 @@ sealed class RecordUiElementState { data object Empty : RecordUiElementState() } +data class SelectUiElementState( + val listItems: List, + val interactionTypes: List>, + val selectedInteractionType: NodeInteractionType?, +) + data class UiElementListItemModel( val id: Long, val nodeViewResourceId: String?, val nodeText: String?, val nodeClassName: String?, val nodeUniqueId: String?, - val interactionTypes: String, + val interactionTypesText: String, + val interactionTypes: Set, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index 0913213f06..5a99a511ca 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -19,7 +19,7 @@ data class AccessibilityNodeEntity( /** * A list of the allowed accessibility node actions. */ - val actions: List, + val actions: Set, /** * The accessibility action id of how the user interacted * with this node. This is null if the user didn't interact with diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index 678f3ec536..d8663c7153 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -83,7 +83,7 @@ class AccessibilityNodeRecorder( } else { null }, - actions = interactionTypes, + actions = interactionTypes.toSet(), userInteractedActionId = userInteractedActionId, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt index f293b15968..cddcfdcd78 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt @@ -10,18 +10,17 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.system.network.HttpMethod @Composable @OptIn(ExperimentalMaterial3Api::class) -fun KeyMapperDropdownMenu( +fun KeyMapperDropdownMenu( modifier: Modifier = Modifier, expanded: Boolean, onExpandedChange: (Boolean) -> Unit = {}, - value: String, - onValueChanged: (String) -> Unit = {}, + label: String, + selectedValue: T, + values: List>, + onValueChanged: (T) -> Unit = {}, ) { ExposedDropdownMenuBox( modifier = modifier, @@ -30,28 +29,31 @@ fun KeyMapperDropdownMenu( ) { TextField( modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), - value = value, - onValueChange = onValueChanged, + value = values.single { it.first == selectedValue }.second, + onValueChange = { newValue -> + onValueChanged(values.single { it.second == newValue }.first) + }, readOnly = true, - label = { Text(stringResource(R.string.action_http_request_method_label)) }, + label = { Text(text = label) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), ) + ExposedDropdownMenu( matchTextFieldWidth = true, expanded = expanded, onDismissRequest = { onExpandedChange(false) }, ) { - for (method in HttpMethod.entries) { + for ((value, valueText) in values) { DropdownMenuItem( text = { Text( - method.toString(), + valueText, style = MaterialTheme.typography.bodyLarge, ) }, onClick = { - onValueChanged(method.toString()) + onValueChanged(value) onExpandedChange(false) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fc86c0576..fcdf7c1fe1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1120,6 +1120,7 @@ Select how you want to interact with the UI element. Filter interaction type + Any Tap Tap and hold Focus From 3e1aba7bcef043d6084eff438b0e361f5ce17f9f Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 17:05:45 +0200 Subject: [PATCH 17/69] #257 complete creating interact UI element actions --- .../sds100/keymapper/actions/ActionData.kt | 2 +- .../actions/ActionDataEntityMapper.kt | 41 ++-- .../keymapper/actions/ActionUiHelper.kt | 2 +- .../sds100/keymapper/actions/ActionUtils.kt | 1 + .../keymapper/actions/CreateActionDelegate.kt | 6 +- .../actions/HttpRequestBottomSheet.kt | 2 +- .../uielement/ChooseUiElementScreen.kt | 2 +- .../uielement/InteractUiElementScreen.kt | 209 +++++++++++++++++- .../uielement/InteractUiElementViewModel.kt | 76 +++++-- .../keymapper/util/ui/NavigationViewModel.kt | 7 + .../util/ui/compose/KeyMapperDropdownMenu.kt | 6 +- app/src/main/res/values/strings.xml | 7 +- 12 files changed, 304 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 9b36222391..3823a2e5f7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -860,7 +860,7 @@ sealed class ActionData : Comparable { /** * A list of the allowed accessibility node actions. */ - val nodeActions: List, + val nodeActions: Set, ) : ActionData() { override val id: ActionId = ActionId.INTERACT_UI_ELEMENT } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 77f6b92d87..3ae0464723 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -293,7 +293,7 @@ object ActionDataEntityMapper { ActionId.TOGGLE_FLASHLIGHT, ActionId.ENABLE_FLASHLIGHT, ActionId.CHANGE_FLASHLIGHT_STRENGTH, - -> { + -> { val lens = entity.extras.getData(ActionEntity.EXTRA_LENS).then { LENS_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null @@ -552,12 +552,17 @@ object ActionDataEntityMapper { entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID).valueOrNull() val actions = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS).then { - val typeList = it - .split(",") - .mapNotNull { action -> convertNodeInteractionType(it).valueOrNull() } - - Success(typeList) - }.valueOrNull() ?: emptyList() + val nodeActionMask = it.toInt() + val interactionTypeSet = mutableSetOf() + + for (type in NodeInteractionType.entries) { + if (nodeActionMask and type.accessibilityActionId == type.accessibilityActionId) { + interactionTypeSet.add(type) + } + } + + Success(interactionTypeSet) + }.valueOrNull() ?: emptySet() val nodeAction = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_NODE_ACTION).then { @@ -820,14 +825,12 @@ object ActionDataEntityMapper { ), ) - data.packageName.let { - add( - EntityExtra( - ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME, - it, - ), - ) - } + add( + EntityExtra( + ActionEntity.EXTRA_ACCESSIBILITY_PACKAGE_NAME, + data.packageName, + ), + ) data.contentDescription?.let { add( @@ -868,10 +871,16 @@ object ActionDataEntityMapper { } if (data.nodeActions.isNotEmpty()) { + var nodeActionMask = 0 + + for (nodeAction in data.nodeActions) { + nodeActionMask = nodeActionMask or nodeAction.accessibilityActionId + } + add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS, - data.nodeActions.joinToString(","), + nodeActionMask.toString(), ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index e782916e4b..ccd1fac039 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -530,7 +530,7 @@ class ActionUiHelper( ActionData.DeviceControls -> getString(R.string.action_device_controls) is ActionData.HttpRequest -> action.description - is ActionData.InteractUiElement -> getString(R.string.action_interact_ui_element_title) + is ActionData.InteractUiElement -> action.description } fun getIcon(action: ActionData): ComposeIconInfo = when (action) { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index 12b23dd67f..a2a0d82c73 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -827,6 +827,7 @@ fun ActionData.isEditable(): Boolean = when (this) { is ActionData.Url, is ActionData.PhoneCall, is ActionData.HttpRequest, + is ActionData.InteractUiElement, -> true else -> false diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index ac94158e21..649ef42bd7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -788,11 +788,7 @@ class CreateActionDelegate( } ActionId.INTERACT_UI_ELEMENT -> { - val oldAction = if (oldData is ActionData.InteractUiElement) { - oldData - } else { - null - } + val oldAction = oldData as? ActionData.InteractUiElement return navigate( "config_interact_ui_element_action", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt index ee00632d66..15d0063245 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt @@ -139,7 +139,7 @@ private fun HttpRequestBottomSheet( .padding(horizontal = 16.dp), expanded = methodExpanded, onExpandedChange = { methodExpanded = it }, - label = stringResource(R.string.action_http_request_method_label), + label = { Text(stringResource(R.string.action_http_request_method_label)) }, selectedValue = state.method, values = HttpMethod.entries.map { it to it.toString() }, onValueChanged = onSelectMethod, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index 3d3355d28b..612cb913af 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -151,7 +151,7 @@ fun ChooseElementScreen( modifier = Modifier.padding(horizontal = 16.dp), expanded = interactionTypeExpanded, onExpandedChange = { interactionTypeExpanded = it }, - label = stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown), + label = { Text(stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown)) }, values = state.data.interactionTypes, selectedValue = state.data.selectedInteractionType, onValueChanged = onSelectInteractionType, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 6d9b6164af..946655a742 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.actions.uielement import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,9 +14,14 @@ import androidx.compose.foundation.layout.fillMaxSize 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.sizeIn import androidx.compose.foundation.layout.width +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.outlined.Info import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material3.BottomAppBar @@ -23,22 +29,29 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.pluralStringResource @@ -50,6 +63,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.compose.LocalCustomColorsPalette @@ -57,7 +71,10 @@ import io.github.sds100.keymapper.system.apps.ChooseAppScreen import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu +import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow import io.github.sds100.keymapper.util.ui.compose.icons.AdGroup +import io.github.sds100.keymapper.util.ui.compose.icons.JumpToElement import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons import kotlinx.coroutines.flow.update @@ -110,6 +127,8 @@ fun InteractUiElementScreen( openSelectAppScreen = { navController.navigate(DEST_SELECT_APP) }, + onSelectInteractionType = viewModel::onSelectElementInteractionType, + onDescriptionChanged = viewModel::onDescriptionChanged, ) } @@ -156,6 +175,8 @@ private fun LandingScreen( onBackClick: () -> Unit = {}, onDoneClick: () -> Unit = {}, openSelectAppScreen: () -> Unit = {}, + onSelectInteractionType: (NodeInteractionType) -> Unit = {}, + onDescriptionChanged: (String) -> Unit = {}, ) { val snackbarHostState = SnackbarHostState() @@ -171,7 +192,12 @@ private fun LandingScreen( ) } }, floatingActionButton = { - if (selectedElementState != null) { + if (selectedElementState == null || selectedElementState.description.isBlank()) { + DisabledExtendedFloatingActionButton( + icon = { Icon(Icons.Rounded.Check, stringResource(R.string.button_done)) }, + text = stringResource(R.string.button_done), + ) + } else { ExtendedFloatingActionButton( onClick = onDoneClick, text = { Text(stringResource(R.string.button_done)) }, @@ -198,7 +224,7 @@ private fun LandingScreen( end = endPadding, ), ) { - Column { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Text( modifier = Modifier.padding( start = 16.dp, @@ -232,13 +258,63 @@ private fun LandingScreen( Spacer(modifier = Modifier.height(8.dp)) if (selectedElementState != null) { - SelectedElementSection(modifier = Modifier.fillMaxWidth(), selectedElementState) + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SelectedElementSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = selectedElementState, + onSelectInteractionType = onSelectInteractionType, + onDescriptionChanged = onDescriptionChanged, + ) } } } } } +@Composable +private fun DisabledExtendedFloatingActionButton( + modifier: Modifier = Modifier, + icon: @Composable () -> Unit, + text: String, +) { + Surface( + modifier = modifier, + shape = FloatingActionButtonDefaults.extendedFabShape, + color = FloatingActionButtonDefaults.containerColor.copy(alpha = 0.5f), + ) { + Row( + modifier = + Modifier + .sizeIn(minWidth = 80.dp, minHeight = 56.dp) + .padding(start = 16.dp, end = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + val contentColor = + MaterialTheme.colorScheme.contentColorFor(FloatingActionButtonDefaults.containerColor) + .copy(alpha = 0.5f) + + CompositionLocalProvider(LocalContentColor provides contentColor) { + icon() + Spacer(Modifier.width(12.dp)) + Text( + text, + style = MaterialTheme.typography.labelLarge, + ) + } + } + } +} + @Composable private fun RecordingSection( modifier: Modifier = Modifier, @@ -382,8 +458,129 @@ private fun RecordButton( } @Composable -private fun SelectedElementSection(modifier: Modifier = Modifier, state: SelectedUiElementState) { +private fun SelectedElementSection( + modifier: Modifier = Modifier, + state: SelectedUiElementState, + onDescriptionChanged: (String) -> Unit = {}, + onSelectInteractionType: (NodeInteractionType) -> Unit = {}, +) { + var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + Column(modifier = modifier) { + val isError = state.description.isBlank() + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = state.description, + onValueChange = onDescriptionChanged, + isError = isError, + supportingText = if (isError) { + { Text(stringResource(R.string.error_cant_be_empty)) } + } else { + null + }, + label = { + Text(stringResource(R.string.action_interact_ui_element_description_label)) + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + icon = Icons.Outlined.Info, + text = stringResource(R.string.action_interact_ui_element_interaction_details_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.action_interact_ui_element_app_label), + style = MaterialTheme.typography.titleSmall, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + if (state.appIcon != null) { + val painter = rememberDrawablePainter(state.appIcon.drawable) + Icon( + modifier = Modifier.size(24.dp), + painter = painter, + contentDescription = null, + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = state.appName, style = MaterialTheme.typography.bodyMedium) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (state.nodeText != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_text_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeText, style = MaterialTheme.typography.bodyMedium) + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (state.nodeClassName != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_class_name_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeClassName, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) + } + + if (state.nodeViewResourceId != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_view_id_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeViewResourceId, style = MaterialTheme.typography.bodyMedium) + + Spacer(modifier = Modifier.height(8.dp)) + } + + if (state.nodeUniqueId != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_unique_id_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeUniqueId, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) + } + + OptionsHeaderRow( + icon = KeyMapperIcons.JumpToElement, + text = stringResource(R.string.action_interact_ui_element_interaction_type_dropdown), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.action_interact_ui_element_interaction_type_dropdown_caption), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperDropdownMenu( + expanded = interactionTypeExpanded, + onExpandedChange = { interactionTypeExpanded = it }, + values = state.interactionTypes, + selectedValue = state.selectedInteraction, + onValueChanged = onSelectInteractionType, + ) + + Spacer(modifier = Modifier.height(8.dp)) } } @@ -407,14 +604,14 @@ private fun PreviewSelectedElement() { LandingScreen( recordState = State.Data(RecordUiElementState.Recorded(3)), selectedElementState = SelectedUiElementState( - description = "Test", + description = "Tap test node", appName = "Test App", appIcon = ComposeIconInfo.Drawable(appIcon), nodeText = "Test Node", nodeClassName = "android.widget.ImageButton", nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", nodeUniqueId = "123", - interactionTypes = listOf(), + interactionTypes = listOf(NodeInteractionType.LONG_CLICK to "Tap and hold"), selectedInteraction = NodeInteractionType.LONG_CLICK, ), ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 23c995622a..db28e58093 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -17,7 +17,6 @@ import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.ifIsData import io.github.sds100.keymapper.util.mapData import io.github.sds100.keymapper.util.onFailure -import io.github.sds100.keymapper.util.otherwise import io.github.sds100.keymapper.util.then import io.github.sds100.keymapper.util.ui.PopupViewModel import io.github.sds100.keymapper.util.ui.PopupViewModelImpl @@ -33,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.Locale class InteractUiElementViewModel( private val useCase: InteractUiElementUseCase, @@ -50,7 +51,7 @@ class InteractUiElementViewModel( PopupViewModel by PopupViewModelImpl() { private val _returnAction: MutableSharedFlow = MutableSharedFlow() - val returnAction: SharedFlow = _returnAction + val returnAction: SharedFlow = _returnAction.asSharedFlow() val recordState: StateFlow> = combine( useCase.recordState, @@ -63,9 +64,16 @@ class InteractUiElementViewModel( val mins = recordState.timeLeft / 60 val secs = recordState.timeLeft % 60 + val timeRemainingText = String.format( + Locale.getDefault(), + "%02d:%02d", + mins, + secs, + ) + State.Data( RecordUiElementState.CountingDown( - timeRemaining = "$mins:$secs", + timeRemaining = timeRemainingText, interactionCount = interactionCount, ), ) @@ -116,7 +124,18 @@ class InteractUiElementViewModel( private val interactionTypesFilterItems: Flow>>> = interactionsByPackage - .map { state -> state.mapData(::buildInteractionTypeFilterItems) } + .map { state -> + state.mapData { list -> + val any = Pair( + null, + getString(R.string.action_interact_ui_element_interaction_type_any), + ) + + val interactionTypes = list.flatMap { it.actions }.toSet() + + listOf(any).plus(buildInteractionTypeFilterItems(interactionTypes)) + } + } private val selectedInteractionTypeFilter = MutableStateFlow(null) @@ -172,7 +191,7 @@ class InteractUiElementViewModel( nodeClassName = action.className, nodeViewResourceId = action.viewResourceId, nodeUniqueId = action.uniqueId, - interactionTypes = action.nodeActions, + interactionTypes = buildInteractionTypeFilterItems(action.nodeActions), selectedInteraction = action.nodeAction, ) @@ -186,6 +205,10 @@ class InteractUiElementViewModel( return } + if (selectedElementState.description.isBlank()) { + return + } + val action = ActionData.InteractUiElement( description = selectedElementState.description, nodeAction = selectedElementState.selectedInteraction, @@ -195,10 +218,12 @@ class InteractUiElementViewModel( className = selectedElementState.nodeClassName, viewResourceId = selectedElementState.nodeViewResourceId, uniqueId = selectedElementState.nodeUniqueId, - nodeActions = selectedElementState.interactionTypes, + nodeActions = selectedElementState.interactionTypes.map { it.first }.toSet(), ) - _returnAction.tryEmit(action) + viewModelScope.launch { + _returnAction.emit(action) + } } fun onRecordClick() { @@ -226,6 +251,9 @@ class InteractUiElementViewModel( useCase.getAppName(interaction.packageName).valueOrNull() ?: interaction.packageName val appIcon = getAppIcon(interaction.packageName) + val selectedInteraction = + NodeInteractionType.entries.first { interaction.actions.contains(it) } + val newState = SelectedUiElementState( description = "", appName = appName, @@ -234,19 +262,30 @@ class InteractUiElementViewModel( nodeClassName = interaction.className, nodeViewResourceId = interaction.viewResourceId, nodeUniqueId = interaction.uniqueId, - interactionTypes = interaction.actions.toList(), - selectedInteraction = interaction.userInteractedActionId - ?: interaction.actions.first(), + interactionTypes = buildInteractionTypeFilterItems(interaction.actions), + selectedInteraction = selectedInteraction, ) _selectedElementState.update { newState } } } + fun onSelectElementInteractionType(interactionType: NodeInteractionType) { + _selectedElementState.update { state -> + state?.copy(selectedInteraction = interactionType) + } + } + fun onSelectInteractionTypeFilter(interactionType: NodeInteractionType?) { selectedInteractionTypeFilter.update { interactionType } } + fun onDescriptionChanged(description: String) { + _selectedElementState.update { state -> + state?.copy(description = description) + } + } + private suspend fun startRecording() { useCase.startRecording().onFailure { error -> if (error == Error.AccessibilityServiceDisabled) { @@ -267,7 +306,7 @@ class InteractUiElementViewModel( private fun buildInteractedPackageListItem(packageName: String): SimpleListItemModel { val appName = useCase.getAppName(packageName).valueOrNull() ?: packageName - val appIcon = getAppIcon(packageName) + val appIcon = getAppIcon(packageName) ?: ComposeIconInfo.Vector(Icons.Rounded.Android) return SimpleListItemModel( id = packageName, @@ -290,12 +329,8 @@ class InteractUiElementViewModel( ) } - private fun buildInteractionTypeFilterItems(nodes: List): List> { - val interactionTypes = nodes.flatMap { it.actions }.toSet() - + private fun buildInteractionTypeFilterItems(interactionTypes: Set): List> { return buildList { - add(null to getString(R.string.action_interact_ui_element_interaction_type_any)) - // They should always be in the same order so iterate over the Enum entries. for (type in NodeInteractionType.entries) { if (interactionTypes.contains(type)) { @@ -305,11 +340,10 @@ class InteractUiElementViewModel( } } - private fun getAppIcon(packageName: String): ComposeIconInfo = useCase + private fun getAppIcon(packageName: String): ComposeIconInfo.Drawable? = useCase .getAppIcon(packageName) .then { Success(ComposeIconInfo.Drawable(it)) } - .otherwise { Success(ComposeIconInfo.Vector(Icons.Rounded.Android)) } - .valueOrNull()!! + .valueOrNull() private fun getInteractionTypeString(interactionType: NodeInteractionType): String { return when (interactionType) { @@ -338,12 +372,12 @@ class InteractUiElementViewModel( data class SelectedUiElementState( val description: String, val appName: String, - val appIcon: ComposeIconInfo, + val appIcon: ComposeIconInfo.Drawable?, val nodeText: String?, val nodeClassName: String?, val nodeViewResourceId: String?, val nodeUniqueId: String?, - val interactionTypes: List, + val interactionTypes: List>, val selectedInteraction: NodeInteractionType, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt index 67d57171b5..fc9c239439 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/NavigationViewModel.kt @@ -20,6 +20,7 @@ import io.github.sds100.keymapper.actions.swipescreen.SwipePickCoordinateResult import io.github.sds100.keymapper.actions.swipescreen.SwipePickDisplayCoordinateFragment import io.github.sds100.keymapper.actions.tapscreen.PickCoordinateResult import io.github.sds100.keymapper.actions.tapscreen.PickDisplayCoordinateFragment +import io.github.sds100.keymapper.actions.uielement.InteractUiElementFragment import io.github.sds100.keymapper.constraints.ChooseConstraintFragment import io.github.sds100.keymapper.constraints.Constraint import io.github.sds100.keymapper.system.apps.ActivityInfo @@ -330,5 +331,11 @@ fun NavigationViewModel.sendNavResultFromBundle( onNavResult(NavResult(requestKey, BluetoothDeviceInfo(address, name))) } + + NavDestination.ID_INTERACT_UI_ELEMENT_ACTION -> { + val json = bundle.getString(InteractUiElementFragment.EXTRA_ACTION)!! + val result = Json.decodeFromString(json) + onNavResult(NavResult(requestKey, result)) + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt index cddcfdcd78..eafba75c5d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt @@ -17,7 +17,7 @@ fun KeyMapperDropdownMenu( modifier: Modifier = Modifier, expanded: Boolean, onExpandedChange: (Boolean) -> Unit = {}, - label: String, + label: (@Composable () -> Unit)? = null, selectedValue: T, values: List>, onValueChanged: (T) -> Unit = {}, @@ -29,12 +29,12 @@ fun KeyMapperDropdownMenu( ) { TextField( modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), - value = values.single { it.first == selectedValue }.second, + value = values.find { it.first == selectedValue }?.second ?: values.first().second, onValueChange = { newValue -> onValueChanged(values.single { it.second == newValue }.first) }, readOnly = true, - label = { Text(text = label) }, + label = label, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fcdf7c1fe1..587ccb6ecd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1131,9 +1131,12 @@ Collapse Unknown: %d - Text - + Interaction details + Description + App + Text / content description Class name + View resource ID Unique ID Interaction types From 670631e594a706e4da000520548f8e7604cc2bb2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 17:24:21 +0200 Subject: [PATCH 18/69] #257 performing interact with UI element actions works --- .../actions/ConfigActionsViewModel.kt | 1 + .../actions/PerformActionsUseCase.kt | 57 ++++++++++++++++--- .../uielement/InteractUiElementScreen.kt | 1 + .../uielement/InteractUiElementViewModel.kt | 5 +- .../sds100/keymapper/util/ErrorUtils.kt | 1 + .../io/github/sds100/keymapper/util/Result.kt | 2 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt index f501900c70..96d6da8bb0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt @@ -177,6 +177,7 @@ class ConfigActionsViewModel( override fun onEditClick() { val actionUid = actionOptionsUid.value ?: return coroutineScope.launch { + actionOptionsUid.update { null } val keyMap = config.keyMap.first().dataOrNull() ?: return@launch val oldAction = keyMap.actionList.find { it.uid == actionUid } ?: return@launch diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 0bb4dc9a59..80c6ea900d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -10,6 +10,7 @@ import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeAction +import io.github.sds100.keymapper.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.accessibility.ServiceAdapter import io.github.sds100.keymapper.system.airplanemode.AirplaneModeAdapter @@ -803,16 +804,15 @@ class PerformActionsUseCaseImpl( is ActionData.InteractUiElement -> { if (accessibilityService.activeWindowPackage.first() != action.packageName) { - // TODO + result = Error.UiElementNotFound + } else { + result = accessibilityService.performActionOnNode( + findNode = { node -> + matchAccessibilityNode(node, action) + }, + performAction = { AccessibilityNodeAction(action = action.nodeAction.accessibilityActionId) }, + ) } - - result = accessibilityService.performActionOnNode( - findNode = { node -> - // TODO compare other values - node.uniqueId == action.uniqueId - }, - performAction = { AccessibilityNodeAction(action = action.nodeAction.accessibilityActionId) }, - ) } } @@ -906,6 +906,45 @@ class PerformActionsUseCaseImpl( popupMessageAdapter.showPopupMessage(it.getFullMessage(resourceProvider)) } } + + private fun matchAccessibilityNode( + node: AccessibilityNodeModel, + action: ActionData.InteractUiElement, + ): Boolean { + if (compareIfNonNull(node.uniqueId, action.uniqueId)) { + return true + } + + val viewResourceIdMatches = node.viewResourceId == action.viewResourceId + val classNameMatches = node.className == action.className + + if (compareIfNonNull( + node.contentDescription, + action.contentDescription, + ) && + viewResourceIdMatches && + classNameMatches + ) { + return true + } + + if (compareIfNonNull(node.text, action.text) && + viewResourceIdMatches && + classNameMatches + ) { + return true + } + + if (viewResourceIdMatches) { + return true + } + + return false + } + + private fun compareIfNonNull(a: T?, b: T?): Boolean { + return a != null && b != null && a == b + } } interface PerformActionsUseCase { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 946655a742..c4fd683a5d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -605,6 +605,7 @@ private fun PreviewSelectedElement() { recordState = State.Data(RecordUiElementState.Recorded(3)), selectedElementState = SelectedUiElementState( description = "Tap test node", + packageName = "com.example.test", appName = "Test App", appIcon = ComposeIconInfo.Drawable(appIcon), nodeText = "Test Node", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index db28e58093..80d255273b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -185,6 +185,7 @@ class InteractUiElementViewModel( val newState = SelectedUiElementState( description = action.description, + packageName = action.packageName, appName = appName, appIcon = appIcon, nodeText = action.text ?: action.contentDescription, @@ -212,7 +213,7 @@ class InteractUiElementViewModel( val action = ActionData.InteractUiElement( description = selectedElementState.description, nodeAction = selectedElementState.selectedInteraction, - packageName = selectedElementState.appName, + packageName = selectedElementState.packageName, text = selectedElementState.nodeText, contentDescription = selectedElementState.nodeText, className = selectedElementState.nodeClassName, @@ -256,6 +257,7 @@ class InteractUiElementViewModel( val newState = SelectedUiElementState( description = "", + packageName = interaction.packageName, appName = appName, appIcon = appIcon, nodeText = interaction.text ?: interaction.contentDescription, @@ -371,6 +373,7 @@ class InteractUiElementViewModel( data class SelectedUiElementState( val description: String, + val packageName: String, val appName: String, val appIcon: ComposeIconInfo.Drawable?, val nodeText: String?, diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt index 4aa7e57171..8a047e25b0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ErrorUtils.kt @@ -162,6 +162,7 @@ fun Error.getFullMessage(resourceProvider: ResourceProvider): String = when (thi Error.DpadTriggerImeNotSelected -> resourceProvider.getString(R.string.trigger_error_dpad_ime_not_selected) Error.InvalidBackup -> resourceProvider.getString(R.string.error_invalid_backup) Error.MalformedUrl -> resourceProvider.getString(R.string.error_malformed_url) + Error.UiElementNotFound -> resourceProvider.getString(R.string.error_ui_element_not_found) } val Error.isFixable: Boolean diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt index 0d904a6685..8fe8251769 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Result.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Result.kt @@ -145,6 +145,8 @@ sealed class Error : Result() { */ data object DpadTriggerImeNotSelected : Error() data object MalformedUrl : Error() + + data object UiElementNotFound : Error() } inline fun Result.onSuccess(f: (T) -> Unit): Result { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 587ccb6ecd..933bc823e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -909,6 +909,8 @@ Must be greater than 0! Must be greater than 0! Must be %d or less! + + UI element not found! From 829634054ce35d0d4753e9a786e95ff4258bee03 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 20:28:19 +0200 Subject: [PATCH 19/69] #257 save nodes to the database --- .../19.json | 441 ++++++++++++++++++ .../github/sds100/keymapper/ServiceLocator.kt | 1 + .../actions/ActionDataEntityMapper.kt | 22 +- .../uielement/InteractUiElementUseCase.kt | 1 + .../sds100/keymapper/data/db/AppDatabase.kt | 12 +- .../data/db/dao/AccessibilityNodeDao.kt | 35 ++ .../NodeInteractionTypeSetTypeConverter.kt | 34 ++ .../data/entities/AccessibilityNodeEntity.kt | 39 +- .../data/migration/AutoMigration18To19.kt | 5 + .../AccessibilityNodeRepository.kt | 61 ++- .../AccessibilityNodeRecorder.kt | 52 ++- 11 files changed, 629 insertions(+), 74 deletions(-) create mode 100644 app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/19.json create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/NodeInteractionTypeSetTypeConverter.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration18To19.kt diff --git a/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/19.json b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/19.json new file mode 100644 index 0000000000..ed9aef420f --- /dev/null +++ b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/19.json @@ -0,0 +1,441 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "300aa53acf7905efdf0c5b0ff7516ec9", + "entities": [ + { + "tableName": "keymaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trigger` TEXT NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, `uid` TEXT NOT NULL, `group_uid` TEXT, FOREIGN KEY(`group_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupUid", + "columnName": "group_uid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_keymaps_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_keymaps_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "fingerprintmaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `extras` TEXT NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `severity` INTEGER NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "floating_layouts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_layouts_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_floating_layouts_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "floating_buttons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `layout_uid` TEXT NOT NULL, `text` TEXT NOT NULL, `button_size` INTEGER NOT NULL, `x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `orientation` TEXT NOT NULL, `display_width` INTEGER NOT NULL, `display_height` INTEGER NOT NULL, `border_opacity` REAL, `background_opacity` REAL, PRIMARY KEY(`uid`), FOREIGN KEY(`layout_uid`) REFERENCES `floating_layouts`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutUid", + "columnName": "layout_uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "buttonSize", + "columnName": "button_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orientation", + "columnName": "orientation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayWidth", + "columnName": "display_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayHeight", + "columnName": "display_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "borderOpacity", + "columnName": "border_opacity", + "affinity": "REAL" + }, + { + "fieldPath": "backgroundOpacity", + "columnName": "background_opacity", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_buttons_layout_uid", + "unique": false, + "columnNames": [ + "layout_uid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_floating_buttons_layout_uid` ON `${TABLE_NAME}` (`layout_uid`)" + } + ], + "foreignKeys": [ + { + "table": "floating_layouts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "layout_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, `constraints` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `parent_uid` TEXT, `last_opened_date` INTEGER, PRIMARY KEY(`uid`), FOREIGN KEY(`parent_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraints", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentUid", + "columnName": "parent_uid", + "affinity": "TEXT" + }, + { + "fieldPath": "lastOpenedDate", + "columnName": "last_opened_date", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "accessibility_nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `text` TEXT, `content_description` TEXT, `class_name` TEXT, `view_resource_id` TEXT, `unique_id` TEXT, `actions` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT" + }, + { + "fieldPath": "contentDescription", + "columnName": "content_description", + "affinity": "TEXT" + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT" + }, + { + "fieldPath": "viewResourceId", + "columnName": "view_resource_id", + "affinity": "TEXT" + }, + { + "fieldPath": "uniqueId", + "columnName": "unique_id", + "affinity": "TEXT" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '300aa53acf7905efdf0c5b0ff7516ec9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt index 342bd175ce..c0dd401af4 100755 --- a/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/ServiceLocator.kt @@ -223,6 +223,7 @@ object ServiceLocator { synchronized(this) { return accessibilityNodeRepository ?: AccessibilityNodeRepositoryImpl( (context.applicationContext as KeyMapperApp).appCoroutineScope, + database(context).accessibilityNodeDao(), ).also { this.accessibilityNodeRepository = it } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 3ae0464723..b6b99e1726 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.actions import io.github.sds100.keymapper.actions.pinchscreen.PinchScreenType import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.db.typeconverter.ConstantTypeConverters +import io.github.sds100.keymapper.data.db.typeconverter.NodeInteractionTypeSetTypeConverter import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.data.entities.EntityExtra import io.github.sds100.keymapper.data.entities.getData @@ -319,7 +320,7 @@ object ActionDataEntityMapper { } ActionId.DISABLE_FLASHLIGHT, - -> { + -> { val lens = entity.extras.getData(ActionEntity.EXTRA_LENS).then { LENS_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null @@ -552,16 +553,7 @@ object ActionDataEntityMapper { entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_UNIQUE_ID).valueOrNull() val actions = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS).then { - val nodeActionMask = it.toInt() - val interactionTypeSet = mutableSetOf() - - for (type in NodeInteractionType.entries) { - if (nodeActionMask and type.accessibilityActionId == type.accessibilityActionId) { - interactionTypeSet.add(type) - } - } - - Success(interactionTypeSet) + Success(NodeInteractionTypeSetTypeConverter().toSet(it.toInt())) }.valueOrNull() ?: emptySet() val nodeAction = @@ -871,16 +863,10 @@ object ActionDataEntityMapper { } if (data.nodeActions.isNotEmpty()) { - var nodeActionMask = 0 - - for (nodeAction in data.nodeActions) { - nodeActionMask = nodeActionMask or nodeAction.accessibilityActionId - } - add( EntityExtra( ActionEntity.EXTRA_ACCESSIBILITY_ACTIONS, - nodeActionMask.toString(), + NodeInteractionTypeSetTypeConverter().toMask(data.nodeActions).toString(), ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt index c1553c6291..9a325f22f3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementUseCase.kt @@ -63,6 +63,7 @@ class InteractUiElementController( override fun getAppIcon(packageName: String): Result = packageManagerAdapter.getAppIcon(packageName) override suspend fun startRecording(): Result<*> { + nodeRepository.deleteAll() return serviceAdapter.send(ServiceEvent.StartRecordingNodes) } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt index e3a933e747..e4644b1055 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt @@ -9,6 +9,7 @@ import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import io.github.sds100.keymapper.data.db.AppDatabase.Companion.DATABASE_VERSION +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao import io.github.sds100.keymapper.data.db.dao.FingerprintMapDao import io.github.sds100.keymapper.data.db.dao.FloatingButtonDao import io.github.sds100.keymapper.data.db.dao.FloatingLayoutDao @@ -18,7 +19,9 @@ import io.github.sds100.keymapper.data.db.dao.LogEntryDao import io.github.sds100.keymapper.data.db.typeconverter.ActionListTypeConverter import io.github.sds100.keymapper.data.db.typeconverter.ConstraintListTypeConverter import io.github.sds100.keymapper.data.db.typeconverter.ExtraListTypeConverter +import io.github.sds100.keymapper.data.db.typeconverter.NodeInteractionTypeSetTypeConverter import io.github.sds100.keymapper.data.db.typeconverter.TriggerTypeConverter +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.data.entities.FingerprintMapEntity import io.github.sds100.keymapper.data.entities.FloatingButtonEntity import io.github.sds100.keymapper.data.entities.FloatingLayoutEntity @@ -28,6 +31,7 @@ import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.migration.AutoMigration14To15 import io.github.sds100.keymapper.data.migration.AutoMigration15To16 import io.github.sds100.keymapper.data.migration.AutoMigration16To17 +import io.github.sds100.keymapper.data.migration.AutoMigration18To19 import io.github.sds100.keymapper.data.migration.Migration10To11 import io.github.sds100.keymapper.data.migration.Migration11To12 import io.github.sds100.keymapper.data.migration.Migration13To14 @@ -44,7 +48,7 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 * Created by sds100 on 24/01/2020. */ @Database( - entities = [KeyMapEntity::class, FingerprintMapEntity::class, LogEntryEntity::class, FloatingLayoutEntity::class, FloatingButtonEntity::class, GroupEntity::class], + entities = [KeyMapEntity::class, FingerprintMapEntity::class, LogEntryEntity::class, FloatingLayoutEntity::class, FloatingButtonEntity::class, GroupEntity::class, AccessibilityNodeEntity::class], version = DATABASE_VERSION, exportSchema = true, autoMigrations = [ @@ -54,6 +58,8 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 AutoMigration(from = 15, to = 16, spec = AutoMigration15To16::class), // This adds last opened timestamp to groups AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class), + // Adds accessibility node table + AutoMigration(from = 18, to = 19, spec = AutoMigration18To19::class), ], ) @TypeConverters( @@ -61,11 +67,12 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 ExtraListTypeConverter::class, TriggerTypeConverter::class, ConstraintListTypeConverter::class, + NodeInteractionTypeSetTypeConverter::class, ) abstract class AppDatabase : RoomDatabase() { companion object { const val DATABASE_NAME = "key_map_database" - const val DATABASE_VERSION = 18 + const val DATABASE_VERSION = 19 val MIGRATION_1_2 = object : Migration(1, 2) { @@ -162,4 +169,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun floatingLayoutDao(): FloatingLayoutDao abstract fun floatingButtonDao(): FloatingButtonDao abstract fun groupDao(): GroupDao + abstract fun accessibilityNodeDao(): AccessibilityNodeDao } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt new file mode 100644 index 0000000000..b86cf032bd --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt @@ -0,0 +1,35 @@ +package io.github.sds100.keymapper.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface AccessibilityNodeDao { + companion object { + const val TABLE_NAME = "accessibility_nodes" + const val KEY_ID = "id" + const val KEY_PACKAGE_NAME = "package_name" + const val KEY_TEXT = "text" + const val KEY_CONTENT_DESCRIPTION = "content_description" + const val KEY_CLASS_NAME = "class_name" + const val KEY_VIEW_RESOURCE_ID = "view_resource_id" + const val KEY_UNIQUE_ID = "unique_id" + const val KEY_ACTIONS = "actions" + } + + @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_ID = (:id)") + suspend fun getById(id: Long): AccessibilityNodeEntity? + + @Query("SELECT * FROM $TABLE_NAME") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.ABORT) + suspend fun insert(vararg node: AccessibilityNodeEntity) + + @Query("DELETE FROM $TABLE_NAME") + suspend fun deleteAll() +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/NodeInteractionTypeSetTypeConverter.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/NodeInteractionTypeSetTypeConverter.kt new file mode 100644 index 0000000000..d4093d82d8 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/typeconverter/NodeInteractionTypeSetTypeConverter.kt @@ -0,0 +1,34 @@ +package io.github.sds100.keymapper.data.db.typeconverter + +import androidx.room.TypeConverter +import io.github.sds100.keymapper.actions.uielement.NodeInteractionType + +/** + * Created by sds100 on 05/09/2018. + */ + +class NodeInteractionTypeSetTypeConverter { + @TypeConverter + fun toSet(mask: Int): Set { + val interactionTypeSet = mutableSetOf() + + for (type in NodeInteractionType.entries) { + if (mask and type.accessibilityActionId == type.accessibilityActionId) { + interactionTypeSet.add(type) + } + } + + return interactionTypeSet + } + + @TypeConverter + fun toMask(set: Set): Int { + var nodeActionMask = 0 + + for (nodeAction in set) { + nodeActionMask = nodeActionMask or nodeAction.accessibilityActionId + } + + return nodeActionMask + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index 5a99a511ca..896a12e8a5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -1,29 +1,48 @@ package io.github.sds100.keymapper.data.entities import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey import io.github.sds100.keymapper.actions.uielement.NodeInteractionType +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ACTIONS +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CLASS_NAME +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CONTENT_DESCRIPTION +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ID +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_PACKAGE_NAME +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_TEXT +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_UNIQUE_ID +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_VIEW_RESOURCE_ID +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.TABLE_NAME import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @Serializable @Parcelize +@Entity(tableName = TABLE_NAME,) data class AccessibilityNodeEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = KEY_ID) val id: Long = 0L, - val parentId: Long? = null, + + @ColumnInfo(name = KEY_PACKAGE_NAME) val packageName: String, + + @ColumnInfo(name = KEY_TEXT) val text: String?, + + @ColumnInfo(name = KEY_CONTENT_DESCRIPTION) val contentDescription: String?, + + @ColumnInfo(name = KEY_CLASS_NAME) val className: String?, + + @ColumnInfo(name = KEY_VIEW_RESOURCE_ID) val viewResourceId: String?, + + @ColumnInfo(name = KEY_UNIQUE_ID) val uniqueId: String?, - /** - * A list of the allowed accessibility node actions. - */ + + @ColumnInfo(name = KEY_ACTIONS) val actions: Set, - /** - * The accessibility action id of how the user interacted - * with this node. This is null if the user didn't interact with - * this node. - */ - val userInteractedActionId: NodeInteractionType?, ) : Parcelable diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration18To19.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration18To19.kt new file mode 100644 index 0000000000..6ee2b50840 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration18To19.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.data.migration + +import androidx.room.migration.AutoMigrationSpec + +class AutoMigration18To19 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt index ee0df25555..8783d66dd5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/AccessibilityNodeRepository.kt @@ -1,45 +1,60 @@ package io.github.sds100.keymapper.data.repositories +import android.database.sqlite.SQLiteConstraintException +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.util.State -import io.github.sds100.keymapper.util.dataOrNull -import io.github.sds100.keymapper.util.mapData import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext interface AccessibilityNodeRepository { val nodes: Flow>> - fun get(id: Long): AccessibilityNodeEntity? - fun insert(node: AccessibilityNodeEntity) - fun deleteAll() + suspend fun get(id: Long): AccessibilityNodeEntity? + fun insert(vararg node: AccessibilityNodeEntity) + suspend fun deleteAll() } -class AccessibilityNodeRepositoryImpl(private val coroutineScope: CoroutineScope) : AccessibilityNodeRepository { - // TODO have a DAO to remember between app launches and so it isn't all cached in memory - also handles IDs automatically. - // TODO do not insert duplicates where all fields are the same - override val nodes = - MutableStateFlow>>(State.Data(ArrayList(128))) +class AccessibilityNodeRepositoryImpl( + private val coroutineScope: CoroutineScope, + private val dao: AccessibilityNodeDao, +) : AccessibilityNodeRepository { - override fun insert(node: AccessibilityNodeEntity) { - nodes.update { currentState -> - currentState.mapData { list -> - val nodeWithId = node.copy(id = list.size + 1L) - list.plus(nodeWithId) + override val nodes: StateFlow>> = + dao.getAll() + .map { list -> + // Distinct by all fields except the ID. + State.Data(list.distinctBy { it.copy(id = 0) }) + } + .flowOn(Dispatchers.IO) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(10000), State.Loading) + + override fun insert(vararg node: AccessibilityNodeEntity) { + coroutineScope.launch(Dispatchers.IO) { + for (n in node) { + try { + dao.insert(n) + } catch (e: SQLiteConstraintException) { + // Do nothing if the node already exists. + } } } } - override fun get(id: Long): AccessibilityNodeEntity? { - val nodes = nodes.value.dataOrNull() ?: return null - return nodes.find { it.id == id } + override suspend fun get(id: Long): AccessibilityNodeEntity? { + return dao.getById(id) } - override fun deleteAll() { - coroutineScope.launch { - nodes.emit(State.Data(ArrayList(128))) + override suspend fun deleteAll() { + withContext(Dispatchers.IO) { + dao.deleteAll() } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index d8663c7153..39fea07d37 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.system.accessibility import android.os.Build import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.entities.AccessibilityNodeEntity import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository @@ -60,34 +61,43 @@ class AccessibilityNodeRecorder( val source = event.source ?: return + val entity = buildNodeEntity(source) ?: return + + val entities = mutableListOf() + entities.add(entity) + + if (source.childCount > 0) { + for (i in 0 until source.childCount) { + val child = source.getChild(i) ?: continue + buildNodeEntity(child)?.also { entities.add(it) } + } + } + + nodeRepository.insert(*entities.toTypedArray()) + } + + private fun buildNodeEntity(source: AccessibilityNodeInfo): AccessibilityNodeEntity? { val interactionTypes = source.actionList.mapNotNull { action -> NodeInteractionType.entries.find { it.accessibilityActionId == action.id } }.distinct() if (interactionTypes.isEmpty()) { - return + return null } - val userInteractedActionId = - NodeInteractionType.entries.find { it.accessibilityActionId == event.action } - - val entity = - AccessibilityNodeEntity( - packageName = event.packageName.toString(), - text = source.text?.toString(), - contentDescription = source.contentDescription?.toString(), - className = source.className?.toString(), - viewResourceId = source.viewIdResourceName, - uniqueId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - source.uniqueId - } else { - null - }, - actions = interactionTypes.toSet(), - userInteractedActionId = userInteractedActionId, - ) - - nodeRepository.insert(entity) + return AccessibilityNodeEntity( + packageName = source.packageName.toString(), + text = source.text?.toString(), + contentDescription = source.contentDescription?.toString(), + className = source.className?.toString(), + viewResourceId = source.viewIdResourceName, + uniqueId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + source.uniqueId + } else { + null + }, + actions = interactionTypes.toSet(), + ) } fun teardown() { From 6841c86be93306b53ac1fa64027248d299c2d485 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 20:43:47 +0200 Subject: [PATCH 20/69] #257 save all nodes within the bounds of the source event node --- .../AccessibilityNodeRecorder.kt | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index 39fea07d37..b0e1250a56 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.system.accessibility +import android.graphics.Rect import android.os.Build import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent @@ -60,20 +61,47 @@ class AccessibilityNodeRecorder( } val source = event.source ?: return + val sourceBounds = Rect() + source.getBoundsInScreen(sourceBounds) - val entity = buildNodeEntity(source) ?: return + val root: AccessibilityNodeInfo = source.window.root ?: return - val entities = mutableListOf() - entities.add(entity) + // This searches for all nodes that are within the bounds of the source of the + // AccessibilityEvent because the source is not necessarily the element + // the user wants to tap. + val entities = getNodesInBounds(root, sourceBounds).toTypedArray() + nodeRepository.insert(*entities) + } + + /** + * Get all the nodes that are within the given bounds. + */ + private fun getNodesInBounds( + node: AccessibilityNodeInfo, + bounds: Rect, + ): Set { + val set = mutableSetOf() + + val nodeBounds = Rect() + node.getBoundsInScreen(nodeBounds) + + if (bounds.contains(nodeBounds)) { + val entity = buildNodeEntity(node) + + if (entity != null) { + set.add(entity) + } + } + + if (node.childCount > 0) { + for (i in 0 until node.childCount) { + val child = node.getChild(i) ?: continue - if (source.childCount > 0) { - for (i in 0 until source.childCount) { - val child = source.getChild(i) ?: continue - buildNodeEntity(child)?.also { entities.add(it) } + set.addAll(getNodesInBounds(child, bounds)) } } - nodeRepository.insert(*entities.toTypedArray()) + return set } private fun buildNodeEntity(source: AccessibilityNodeInfo): AccessibilityNodeEntity? { From 44b66adc63d6ad9e5759674d2258055ddfcbe4bf Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 20:45:29 +0200 Subject: [PATCH 21/69] #257 delete TODOs --- .../main/java/io/github/sds100/keymapper/backup/BackupContent.kt | 1 - .../system/accessibility/BaseAccessibilityServiceController.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupContent.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupContent.kt index d9664aaba6..07a71956d1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupContent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupContent.kt @@ -6,7 +6,6 @@ import io.github.sds100.keymapper.data.entities.FloatingLayoutEntity import io.github.sds100.keymapper.data.entities.GroupEntity import io.github.sds100.keymapper.data.entities.KeyMapEntity -// TODO back up groups that are referenced by key maps - back up all the children as well. If the parent is not included in the back up then set the parent uid to null data class BackupContent( @SerializedName(NAME_DB_VERSION) val dbVersion: Int, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index d2206d813e..d0a3347b75 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -573,7 +573,6 @@ abstract class BaseAccessibilityServiceController( } private fun recordTriggerJob() = coroutineScope.launch { - // TODO use Kotlin timer. repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> if (isActive) { val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration From b96fca3eb170880dcbf112b215de5a0185abf9a4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 20:50:04 +0200 Subject: [PATCH 22/69] #257 add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e501ffdd2..c585e6f557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Added - #699 Time constraints ⏰ +- #257 Action to interact with user interface elements inside other apps. ## Changed From 8c8e0206dcbb3ba9c9cc6252e0b460b940936d3e Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 21:03:23 +0200 Subject: [PATCH 23/69] #257 fix style and free build compilation issues --- .../system/accessibility/AccessibilityServiceController.kt | 5 ++++- .../sds100/keymapper/actions/ActionDataEntityMapper.kt | 2 +- .../sds100/keymapper/actions/FlashlightActionBottomSheet.kt | 2 -- .../io/github/sds100/keymapper/constraints/ConstraintId.kt | 2 +- .../sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt | 2 +- .../keymapper/data/entities/AccessibilityNodeEntity.kt | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 3d2a18d9cf..8e0fb9dda5 100644 --- a/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/free/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.system.accessibility import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.data.repositories.AccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.mappings.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.mappings.PauseKeyMapsUseCase @@ -17,7 +18,7 @@ import kotlinx.coroutines.flow.SharedFlow class AccessibilityServiceController( coroutineScope: CoroutineScope, - accessibilityService: IAccessibilityService, + accessibilityService: MyAccessibilityService, inputEvents: SharedFlow, outputEvents: MutableSharedFlow, detectConstraintsUseCase: DetectConstraintsUseCase, @@ -30,6 +31,7 @@ class AccessibilityServiceController( suAdapter: SuAdapter, inputMethodAdapter: InputMethodAdapter, settingsRepository: PreferenceRepository, + nodeRepository: AccessibilityNodeRepository, ) : BaseAccessibilityServiceController( coroutineScope, accessibilityService, @@ -45,4 +47,5 @@ class AccessibilityServiceController( suAdapter, inputMethodAdapter, settingsRepository, + nodeRepository, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index b6b99e1726..ad26f41cdd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -329,7 +329,7 @@ object ActionDataEntityMapper { ActionId.TOGGLE_DND_MODE, ActionId.ENABLE_DND_MODE, - -> { + -> { val dndMode = entity.extras.getData(ActionEntity.EXTRA_DND_MODE).then { DND_MODE_MAP.getKey(it)!!.success() }.valueOrNull() ?: return null diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt index 346dc1114b..1482dd5327 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/FlashlightActionBottomSheet.kt @@ -35,9 +35,7 @@ import androidx.compose.material3.SheetValue import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TimePicker import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope diff --git a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt index 0c7f2678ae..b015752aca 100644 --- a/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/constraints/ConstraintId.kt @@ -49,5 +49,5 @@ enum class ConstraintId { CHARGING, DISCHARGING, - TIME + TIME, } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt index b86cf032bd..ed2bd36250 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt @@ -32,4 +32,4 @@ interface AccessibilityNodeDao { @Query("DELETE FROM $TABLE_NAME") suspend fun deleteAll() -} +} diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index 896a12e8a5..ca3ba0ee1c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -19,7 +19,7 @@ import kotlinx.serialization.Serializable @Serializable @Parcelize -@Entity(tableName = TABLE_NAME,) +@Entity(tableName = TABLE_NAME) data class AccessibilityNodeEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = KEY_ID) From 00480dd4bf03d8fcaefde84e4d1695743491c2dc Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 21:18:22 +0200 Subject: [PATCH 24/69] #257 fix tests and style --- .../sds100/keymapper/actions/ActionDataEntityMapper.kt | 2 +- .../java/io/github/sds100/keymapper/backup/BackupManager.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index ad26f41cdd..4133b18d4d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -356,7 +356,7 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, - -> { + -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_PACKAGE_NAME).valueOrNull() ?: return null diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index 659c66b79e..5f0667e8f4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -225,7 +225,7 @@ class BackupManagerImpl( val deviceInfoList by rootElement.byNullableArray(BackupContent.NAME_DEVICE_INFO) val migratedKeyMapList = mutableListOf() - + ac val keyMapMigrations = listOf( JsonMigration(9, 10) { json -> Migration9To10.migrateJson(json) }, JsonMigration(10, 11) { json -> Migration10To11.migrateJson(json) }, @@ -249,6 +249,9 @@ class BackupManagerImpl( // Do nothing. It just removed the group name index. JsonMigration(17, 18) { json -> json }, + + // Do nothing. Just added the accessibility node table. + JsonMigration(18, 19) { json -> json }, ) if (keyMapListJsonArray != null) { From 85bba021d9e5cc9d3f60f933a30cbddf2750117c Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 21:49:07 +0200 Subject: [PATCH 25/69] #257 delete random characters --- .../java/io/github/sds100/keymapper/backup/BackupManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index 5f0667e8f4..64bf0f770c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -225,7 +225,7 @@ class BackupManagerImpl( val deviceInfoList by rootElement.byNullableArray(BackupContent.NAME_DEVICE_INFO) val migratedKeyMapList = mutableListOf() - ac + val keyMapMigrations = listOf( JsonMigration(9, 10) { json -> Migration9To10.migrateJson(json) }, JsonMigration(10, 11) { json -> Migration10To11.migrateJson(json) }, From 38b3f09d0d9a7c4dd5975e538795c24cf4050ab0 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 4 May 2025 22:47:19 +0200 Subject: [PATCH 26/69] #257 fix style --- .../java/io/github/sds100/keymapper/backup/BackupManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index 64bf0f770c..f2e22b2ce8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -225,7 +225,7 @@ class BackupManagerImpl( val deviceInfoList by rootElement.byNullableArray(BackupContent.NAME_DEVICE_INFO) val migratedKeyMapList = mutableListOf() - + val keyMapMigrations = listOf( JsonMigration(9, 10) { json -> Migration9To10.migrateJson(json) }, JsonMigration(10, 11) { json -> Migration10To11.migrateJson(json) }, From 684de16c9d7421a2b97f143e8fbd9f9a2d5847d6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 5 May 2025 00:18:27 +0200 Subject: [PATCH 27/69] chore: update version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index c6beedf368..977745561c 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=104 +VERSION_CODE=105 VERSION_NUM=0 \ No newline at end of file From 7b11e4c894588d2c0e03b91792573f483b0afc72 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 5 May 2025 00:21:36 +0200 Subject: [PATCH 28/69] fix: editing actions works again --- .../io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt index 96d6da8bb0..f501900c70 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt @@ -177,7 +177,6 @@ class ConfigActionsViewModel( override fun onEditClick() { val actionUid = actionOptionsUid.value ?: return coroutineScope.launch { - actionOptionsUid.update { null } val keyMap = config.keyMap.first().dataOrNull() ?: return@launch val oldAction = keyMap.actionList.find { it.uid == actionUid } ?: return@launch From 3d4b5c9a04fc13661a079f34ef7162de7b3093bd Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 5 May 2025 00:23:25 +0200 Subject: [PATCH 29/69] chore: update whats new --- app/src/main/assets/whats-new.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 7c29bd65d8..2d54ecfa7c 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1,4 +1,10 @@ -Key Mapper 3.0 is here! 🎉 +** 3.1 features ** + +⏰ Time constraints. + +🔎 Action to interact with app elements. + +** 3.0 features ** 🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. From 0cd1c3455abd8043095c218141b62fbc272f4040 Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 5 May 2025 19:11:13 +0200 Subject: [PATCH 30/69] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 977745561c..9fa7b22caa 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=105 +VERSION_CODE=107 VERSION_NUM=0 \ No newline at end of file From ee0e2251f9346ecf5598c431c33aa7bb1ed02b43 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 15:14:23 +0200 Subject: [PATCH 31/69] update strings for interact with ui element screen --- .../actions/uielement/InteractUiElementScreen.kt | 2 +- app/src/main/res/values/strings.xml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index c4fd683a5d..e2d1a13d70 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -389,7 +389,7 @@ private fun InteractionCountBox( Column(modifier = Modifier.weight(1f)) { Text( pluralStringResource( - R.plurals.action_interact_ui_element_interactions_detected, + R.plurals.action_interact_ui_element_elements_detected, interactionCount, interactionCount, ), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 933bc823e1..295eef2f03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1104,13 +1104,12 @@ You must prepend \'Bearer\' if necessary Interact with app element - Key Mapper can detect and interact with app elements like menus, tabs, buttons and checkboxes. You need to record yourself interacting with the app element so that Key Mapper knows what you want to do. + Key Mapper can detect and interact with app elements like menus, tabs, buttons and checkboxes. You need to record yourself interacting with the app so that Key Mapper knows what you want to do. Start recording Stop recording (%s min left) - Go to another app and interact with it. Key Mapper will record what you do and you can choose which interactions you want to use in your key map. Open Key Mapper again when you’re done. - - %d interaction detected - %d interactions detected + + %d element detected + %d elements detected Choose the app to interact with Record again @@ -1118,7 +1117,7 @@ Choose the element you want your key map to interact with. Can\'t find what you’re looking for? Not all apps are compatible. For incompatible apps you can try the Tap Screen action instead. - Interaction type + Possible interactions Select how you want to interact with the UI element. Filter interaction type From 7438c6d9c8be83b84fb001131ac174e788b73f08 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 15:14:28 +0200 Subject: [PATCH 32/69] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 9fa7b22caa..a9644d9a6e 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=107 +VERSION_CODE=109 VERSION_NUM=0 \ No newline at end of file From d1064ce37b3205af45f1bc1b0474edc25792f6f4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 15:16:23 +0200 Subject: [PATCH 33/69] fastlane: update app title to be title case --- fastlane/metadata/android/en-US/title.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt index 4efb85f341..9810cafe1e 100644 --- a/fastlane/metadata/android/en-US/title.txt +++ b/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -Key Mapper & Floating buttons \ No newline at end of file +Key Mapper & Floating Buttons \ No newline at end of file From f3f47a31c9d261dbf4ace3f0b054b981189ea08b Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 15:18:14 +0200 Subject: [PATCH 34/69] chore: update whats new --- app/src/main/assets/whats-new.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 2d54ecfa7c..791c6929e6 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1,10 +1,8 @@ -** 3.1 features ** - ⏰ Time constraints. 🔎 Action to interact with app elements. -** 3.0 features ** +== 3.0 features == 🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. From 8b4c59b7a77223194d2965bd5a60fccbe677a6fc Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 20:17:07 +0200 Subject: [PATCH 35/69] fix: use correct app title for choosing app element app and single line description --- .../keymapper/actions/uielement/InteractUiElementScreen.kt | 6 ++++-- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index e2d1a13d70..482b679396 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -135,7 +135,7 @@ fun InteractUiElementScreen( composable(DEST_SELECT_APP) { ChooseAppScreen( modifier = Modifier.fillMaxSize(), - title = stringResource(R.string.action_interact_ui_element_choose_element_title), + title = stringResource(R.string.action_interact_ui_element_choose_app_title), state = appListState, query = appSearchQuery, onQueryChange = { query -> viewModel.appSearchQuery.update { query } }, @@ -397,7 +397,7 @@ private fun InteractionCountBox( ) Text( - stringResource(R.string.action_interact_ui_element_choose_interaction), + stringResource(R.string.action_interact_ui_element_choose_app_title), style = MaterialTheme.typography.bodyMedium, ) } @@ -474,6 +474,8 @@ private fun SelectedElementSection( value = state.description, onValueChange = onDescriptionChanged, isError = isError, + maxLines = 1, + singleLine = true, supportingText = if (isError) { { Text(stringResource(R.string.error_cant_be_empty)) } } else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 295eef2f03..2b44780b4e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1111,7 +1111,7 @@ %d element detected %d elements detected - Choose the app to interact with + Choose the app to interact with Record again Choose app element Choose the element you want your key map to interact with. From 9a926b7f8161741e58dd0fe41f5e2fc9782e114e Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 20:18:55 +0200 Subject: [PATCH 36/69] fix: max 1 line for action/constraint shortcut buttons --- .../keymapper/mappings/keymaps/ShortcutRow.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt index d8a147549a..6c157e7ace 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter @@ -97,6 +98,8 @@ private fun ShortcutButton( text = text, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } @@ -140,3 +143,24 @@ private fun PreviewDrawable() { } } } + +@Preview +@Composable +private fun PreviewMultipleLines() { + val ctx = LocalContext.current + val icon = ctx.drawable(R.mipmap.ic_launcher_round) + + KeyMapperTheme { + Surface { + ShortcutRow( + shortcuts = setOf( + ShortcutModel( + icon = ComposeIconInfo.Drawable(icon), + text = "Line 1\nLine 2\nLine 3", + data = TriggerKeyShortcut.FINGERPRINT_GESTURE, + ), + ), + ) + } + } +} From 1ea2b26074c70ced263e93bf459015015c8a3af9 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 21:15:43 +0200 Subject: [PATCH 37/69] refactor: move draggable compose code to utils/ui/compose --- .../java/io/github/sds100/keymapper/actions/ActionListItem.kt | 2 +- .../java/io/github/sds100/keymapper/actions/ActionsScreen.kt | 4 ++-- .../keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt | 2 +- .../keymapper/mappings/keymaps/trigger/TriggerScreen.kt | 4 ++-- .../github/sds100/keymapper/sorting/SortBottomSheetContent.kt | 4 ++-- .../{compose/draggable => util/ui/compose}/DragDropState.kt | 2 +- .../{compose/draggable => util/ui/compose}/DraggableItem.kt | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename app/src/main/java/io/github/sds100/keymapper/{compose/draggable => util/ui/compose}/DragDropState.kt (99%) rename app/src/main/java/io/github/sds100/keymapper/{compose/draggable => util/ui/compose}/DraggableItem.kt (95%) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt index 0ce11b41af..22152922a2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.compose.draggable.DragDropState +import io.github.sds100.keymapper.util.ui.compose.DragDropState import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt index 003235c51e..50acaf1326 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow import io.github.sds100.keymapper.system.camera.CameraLens diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt index 46f27f7661..f93f20f4ef 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.compose.draggable.DragDropState +import io.github.sds100.keymapper.util.ui.compose.DragDropState import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.FingerprintGestureType import io.github.sds100.keymapper.util.ui.LinkType diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt index c56c550109..aa778801b6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt @@ -39,8 +39,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowHeightSizeClass import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow diff --git a/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt b/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt index fb80600a7d..fbfedd964e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/sorting/SortBottomSheetContent.kt @@ -70,8 +70,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.compose.draggable.DraggableItem -import io.github.sds100.keymapper.compose.draggable.rememberDragDropState +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt similarity index 99% rename from app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt rename to app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt index 8d6d6a345a..2dfb597db2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DragDropState.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DragDropState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.compose.draggable +package io.github.sds100.keymapper.util.ui.compose import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring diff --git a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt similarity index 95% rename from app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt rename to app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt index 3bdf4f3fd4..b0653c9fb4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/compose/draggable/DraggableItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/DraggableItem.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.compose.draggable +package io.github.sds100.keymapper.util.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope From 4d9aa3157e264ba694aee751c7fbc7858572ae67 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 22:31:36 +0200 Subject: [PATCH 38/69] #1663 feat: add action to stop media playback --- .../sds100/keymapper/actions/ActionData.kt | 10 ++++++++ .../actions/ActionDataEntityMapper.kt | 12 ++++++++++ .../sds100/keymapper/actions/ActionId.kt | 2 ++ .../keymapper/actions/ActionUiHelper.kt | 3 +++ .../sds100/keymapper/actions/ActionUtils.kt | 9 +++++++ .../keymapper/actions/CreateActionDelegate.kt | 5 ++++ .../actions/PerformActionsUseCase.kt | 10 +++++++- .../keymapper/mappings/PauseKeyMapsUseCase.kt | 2 +- .../system/media/AndroidMediaAdapter.kt | 24 ++++++++++--------- .../keymapper/system/media/MediaAdapter.kt | 5 ++-- app/src/main/res/values/strings.xml | 10 +++++--- 11 files changed, 74 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 3823a2e5f7..cb9ea44868 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -365,6 +365,11 @@ sealed class ActionData : Comparable { data class Rewind(override val packageName: String) : ControlMediaForApp() { override val id = ActionId.REWIND_PACKAGE } + + @Serializable + data class Stop(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STOP_MEDIA_PACKAGE + } } @Serializable @@ -403,6 +408,11 @@ sealed class ActionData : Comparable { data object Rewind : ControlMedia() { override val id = ActionId.REWIND } + + @Serializable + data object Stop : ControlMedia() { + override val id = ActionId.STOP_MEDIA + } } @Serializable diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 4133b18d4d..fc60df2b62 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -356,6 +356,7 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, + ActionId.STOP_MEDIA_PACKAGE, -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_PACKAGE_NAME).valueOrNull() @@ -383,6 +384,9 @@ object ActionDataEntityMapper { ActionId.REWIND_PACKAGE -> ActionData.ControlMediaForApp.Rewind(packageName) + ActionId.STOP_MEDIA_PACKAGE -> + ActionData.ControlMediaForApp.Stop(packageName) + else -> throw Exception("don't know how to create system action for $actionId") } } @@ -461,6 +465,7 @@ object ActionDataEntityMapper { ActionId.PREVIOUS_TRACK -> ActionData.ControlMedia.PreviousTrack ActionId.FAST_FORWARD -> ActionData.ControlMedia.FastForward ActionId.REWIND -> ActionData.ControlMedia.Rewind + ActionId.STOP_MEDIA -> ActionData.ControlMedia.Stop ActionId.GO_BACK -> ActionData.GoBack ActionId.GO_HOME -> ActionData.GoHome @@ -638,6 +643,11 @@ object ActionDataEntityMapper { is ActionData.Url -> data.url is ActionData.Sound -> data.soundUid is ActionData.InteractUiElement -> data.description + is ActionData.ControlMediaForApp.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMediaForApp.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.ControlMedia.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!! + is ActionData.GoBack -> SYSTEM_ACTION_ID_MAP[data.id]!! else -> SYSTEM_ACTION_ID_MAP[data.id]!! } @@ -982,6 +992,8 @@ object ActionDataEntityMapper { ActionId.FAST_FORWARD_PACKAGE to "fast_forward_package", ActionId.REWIND to "rewind", ActionId.REWIND_PACKAGE to "rewind_package", + ActionId.STOP_MEDIA to "stop_media", + ActionId.STOP_MEDIA_PACKAGE to "stop_media_package", ActionId.GO_BACK to "go_back", ActionId.GO_HOME to "go_home", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt index 011c7f825a..e1fa780d7f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt @@ -79,6 +79,8 @@ enum class ActionId { FAST_FORWARD_PACKAGE, REWIND, REWIND_PACKAGE, + STOP_MEDIA, + STOP_MEDIA_PACKAGE, GO_BACK, GO_HOME, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index ccd1fac039..ea942177b8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -222,6 +222,7 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package_formatted is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package_formatted } getString(resId, appName) @@ -235,6 +236,7 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package } getString(resId) @@ -453,6 +455,7 @@ class ActionUiHelper( ActionData.ControlMedia.PlayPause -> getString(R.string.action_play_pause_media) ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) + ActionData.ControlMedia.Stop -> getString(R.string.action_stop_media) ActionData.CopyText -> getString(R.string.action_text_copy) ActionData.CutText -> getString(R.string.action_text_cut) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index a2a0d82c73..55073a03fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -55,6 +55,7 @@ import androidx.compose.material.icons.outlined.SkipPrevious import androidx.compose.material.icons.outlined.Splitscreen import androidx.compose.material.icons.outlined.StayCurrentLandscape import androidx.compose.material.icons.outlined.StayCurrentPortrait +import androidx.compose.material.icons.outlined.StopCircle import androidx.compose.material.icons.outlined.Swipe import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material.icons.outlined.ViewArray @@ -182,6 +183,8 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> ActionCategory.MEDIA ActionId.REWIND -> ActionCategory.MEDIA ActionId.REWIND_PACKAGE -> ActionCategory.MEDIA + ActionId.STOP_MEDIA -> ActionCategory.MEDIA + ActionId.STOP_MEDIA_PACKAGE -> ActionCategory.MEDIA ActionId.GO_BACK -> ActionCategory.NAVIGATION ActionId.GO_HOME -> ActionCategory.NAVIGATION @@ -291,6 +294,8 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> R.string.action_fast_forward_package ActionId.REWIND -> R.string.action_rewind ActionId.REWIND_PACKAGE -> R.string.action_rewind_package + ActionId.STOP_MEDIA -> R.string.action_stop_media + ActionId.STOP_MEDIA_PACKAGE -> R.string.action_stop_media_package ActionId.GO_BACK -> R.string.action_go_back ActionId.GO_HOME -> R.string.action_go_home ActionId.OPEN_RECENTS -> R.string.action_open_recents @@ -404,6 +409,8 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> R.drawable.ic_outline_fast_forward_24 ActionId.REWIND -> R.drawable.ic_outline_fast_rewind_24 ActionId.REWIND_PACKAGE -> R.drawable.ic_outline_fast_rewind_24 + ActionId.STOP_MEDIA -> R.drawable.ic_outline_pause_24 + ActionId.STOP_MEDIA_PACKAGE -> R.drawable.ic_outline_pause_24 ActionId.GO_BACK -> R.drawable.ic_baseline_arrow_back_24 ActionId.GO_HOME -> R.drawable.ic_outline_home_24 ActionId.OPEN_RECENTS -> null @@ -721,6 +728,8 @@ object ActionUtils { ActionId.FAST_FORWARD_PACKAGE -> Icons.Outlined.FastForward ActionId.REWIND -> Icons.Outlined.FastRewind ActionId.REWIND_PACKAGE -> Icons.Outlined.FastRewind + ActionId.STOP_MEDIA -> Icons.Outlined.StopCircle + ActionId.STOP_MEDIA_PACKAGE -> Icons.Outlined.StopCircle ActionId.GO_BACK -> Icons.AutoMirrored.Outlined.ArrowBack ActionId.GO_HOME -> Icons.Outlined.Home ActionId.OPEN_RECENTS -> Icons.Outlined.ViewArray diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index 649ef42bd7..bad924c888 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -181,6 +181,7 @@ class CreateActionDelegate( ActionId.PREVIOUS_TRACK_PACKAGE, ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, + ActionId.STOP_MEDIA_PACKAGE, -> { val packageName = navigate( @@ -211,6 +212,9 @@ class CreateActionDelegate( ActionId.REWIND_PACKAGE -> ActionData.ControlMediaForApp.Rewind(packageName) + ActionId.STOP_MEDIA_PACKAGE -> + ActionData.ControlMediaForApp.Stop(packageName) + else -> throw Exception("don't know how to create action for $actionId") } @@ -729,6 +733,7 @@ class CreateActionDelegate( ActionId.PREVIOUS_TRACK -> return ActionData.ControlMedia.PreviousTrack ActionId.FAST_FORWARD -> return ActionData.ControlMedia.FastForward ActionId.REWIND -> return ActionData.ControlMedia.Rewind + ActionId.STOP_MEDIA -> return ActionData.ControlMedia.Stop ActionId.GO_BACK -> return ActionData.GoBack ActionId.GO_HOME -> return ActionData.GoHome diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 80c6ea900d..551a3b5154 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -223,6 +223,10 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.rewind(action.packageName) } + is ActionData.ControlMediaForApp.Stop -> { + result = mediaAdapter.stop(action.packageName) + } + is ActionData.Rotation.CycleRotations -> { result = displayAdapter.disableAutoRotate().then { val currentOrientation = displayAdapter.cachedOrientation @@ -340,7 +344,7 @@ class PerformActionsUseCaseImpl( is ActionData.Sound -> { result = soundsManager.getSound(action.soundUid).then { file -> - mediaAdapter.playSoundFile(file.uri, VolumeStream.ACCESSIBILITY) + mediaAdapter.playFile(file.uri, VolumeStream.ACCESSIBILITY) } } @@ -547,6 +551,10 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.rewind() } + is ActionData.ControlMedia.Stop -> { + result = mediaAdapter.stop() + } + is ActionData.GoBack -> { result = accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt index 3214f13d19..82c4dc88fe 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/PauseKeyMapsUseCase.kt @@ -21,7 +21,7 @@ class PauseKeyMapsUseCaseImpl( override fun pause() { preferenceRepository.set(Keys.mappingsPaused, true) - mediaAdapter.stopMedia() + mediaAdapter.stopFileMedia() Timber.d("Pause mappings") } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt index 7fc9bebaf7..7ca1d7cba6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt @@ -82,6 +82,18 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me override fun nextTrack(packageName: String?): Result<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, packageName) + override fun stop(packageName: String?): Result<*> = sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, packageName) + + override fun stopFileMedia(): Result<*> { + synchronized(mediaPlayerLock) { + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + } + + return Success(Unit) + } + override fun getActiveMediaSessionPackages(): List { return activeMediaSessions.value .filter { it.playbackState?.state == PlaybackState.STATE_PLAYING } @@ -105,7 +117,7 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me return audioVolumeControlStreams } - override fun playSoundFile(uri: String, stream: VolumeStream): Result<*> { + override fun playFile(uri: String, stream: VolumeStream): Result<*> { try { synchronized(mediaPlayerLock) { mediaPlayer?.stop() @@ -147,16 +159,6 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me } } - override fun stopMedia(): Result<*> { - synchronized(mediaPlayerLock) { - mediaPlayer?.stop() - mediaPlayer?.release() - mediaPlayer = null - } - - return Success(Unit) - } - fun onActiveMediaSessionChange(mediaSessions: List) { activeMediaSessions.update { mediaSessions } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt index d990c9afe5..4c778078af 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt @@ -35,7 +35,8 @@ interface MediaAdapter { fun playPause(packageName: String? = null): Result<*> fun previousTrack(packageName: String? = null): Result<*> fun nextTrack(packageName: String? = null): Result<*> + fun stop(packageName: String? = null): Result<*> - fun playSoundFile(uri: String, stream: VolumeStream): Result<*> - fun stopMedia(): Result<*> + fun playFile(uri: String, stream: VolumeStream): Result<*> + fun stopFileMedia(): Result<*> } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b44780b4e..f10c306f79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -981,11 +981,11 @@ Next track Next track for an app - Next track for %s> + Next track for %s Previous track Previous track for an app - Previous track for %s> + Previous track for %s Fast forward Fast forward for an app @@ -994,9 +994,13 @@ Rewind Rewind for an app - Rewind for %s> + Rewind for %s Not all media apps support rewinding. E.g Google Play Music. + Stop media + Stop media for an app + Stop media for %s + Go back Go home Open recents From 0279bbd04eb56ab8599cb813010450a2ef87fe3d Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 22:33:22 +0200 Subject: [PATCH 39/69] chore: create Cursor IDE rule for creating an action --- .cursor/rules/create-action.mdc | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .cursor/rules/create-action.mdc diff --git a/.cursor/rules/create-action.mdc b/.cursor/rules/create-action.mdc new file mode 100644 index 0000000000..e8b352f9db --- /dev/null +++ b/.cursor/rules/create-action.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: false +--- +[ActionEntity.kt](mdc:app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt) [ActionDataEntityMapper.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt) [ActionData.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt) [ActionId.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt) [PerformActionsUseCase.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt) [ActionUtils.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt) [strings.xml](mdc:app/src/main/res/values/strings.xml) [ActionUiHelper.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt) [CreateActionDelegate.kt](mdc:app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt) + + +When you create an action you must follow these steps: + +0. Ask me whether the action is editable. +1. Create a new id in ActionId +2. Create a new ActionData +3. Map the data to and from an entity in ActionDataEntityMapper +4. Give the action a category in ActionUtils +5. If the action is editable then add it to the isEditable function in ActionUtils +6. Create a title for the action in strings.xml +7. Give the action a title and icon in ActionUtils. Only create a compose Icon. Ignore the drawable one. +8. Give the action a title in ActionUiHelper +9. Stub out the action in PerformActionsUseCase +10. Handle creating the action in CreateActionDelegate + +Important things to remember: + +- Do not delete any existing code for other actions. +- Follow the naming of existing code and strings and do not change them. +- Add code near existing code for similar actions. \ No newline at end of file From 334e624cffede2f70568f5553a4d4c83c51cc8c6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 22:33:29 +0200 Subject: [PATCH 40/69] chore: create Cursor IDE rule for jetpack compose code --- .cursor/rules/jetpack-compose.mdc | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .cursor/rules/jetpack-compose.mdc diff --git a/.cursor/rules/jetpack-compose.mdc b/.cursor/rules/jetpack-compose.mdc new file mode 100644 index 0000000000..ef3b89eb3a --- /dev/null +++ b/.cursor/rules/jetpack-compose.mdc @@ -0,0 +1,71 @@ +--- +description: +globs: +alwaysApply: false +--- + +# Android Native (Jetpack Compose) + +# Android Jetpack Compose .cursorrules + +## Flexibility Notice +**Note:** This is a recommended project structure, but be flexible and adapt to existing project structures. Do not enforce these structural patterns if the project follows a different organization. Focus on maintaining consistency with the existing project architecture while applying Jetpack Compose best practices. + +## Project Architecture and Best Practices +```kotlin +val androidJetpackComposeBestPractices = listOf( + "Adapt to existing project architecture while maintaining clean code principles", + "Follow Material Design 3 guidelines and components", + "Implement clean architecture with domain, data, and presentation layers", + "Use Kotlin coroutines and Flow for asynchronous operations", + "Implement dependency injection using Hilt", + "Follow unidirectional data flow with ViewModel and UI State", + "Use Compose navigation for screen management", + "Implement proper state hoisting and composition" +) +``` + +## Folder Structure +**Note:** This is a reference structure. Adapt to the project's existing organization. +```plaintext +app/ + src/ + main/ + java/io/github/sds100/keymapper + actions/ + constraints/ + groups/ + home/ + mappings/ + keymaps/ + utils/ + res/ + values/ + drawable/ + mipmap/ + test/ + androidTest/ +``` + +## Compose UI Guidelines +1. Use `remember` and `derivedStateOf` appropriately +2. Implement proper recomposition optimization +3. Use proper Compose modifiers ordering +4. Follow composable function naming conventions +5. Implement proper preview annotations +6. Use proper state management with `MutableState` +7. Implement proper error handling and loading states +8. Use proper theming with `MaterialTheme` +9. Follow accessibility guidelines +10. Implement proper animation patterns + +## Performance Guidelines +1. Minimize recomposition using proper keys +2. Use proper lazy loading with `LazyColumn` and `LazyRow` +3. Implement efficient image loading +4. Use proper state management to prevent unnecessary updates +5. Follow proper lifecycle awareness +6. Implement proper memory management +7. Use proper background processing + + From 6375136e6ceffe261ab2a448845cafdb9e73a555 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 23:43:40 +0200 Subject: [PATCH 41/69] #1663 feat: add action to step forward/backward media --- CHANGELOG.md | 1 + .../sds100/keymapper/actions/ActionData.kt | 20 +++++++++++++++++++ .../actions/ActionDataEntityMapper.kt | 14 +++++++++++++ .../sds100/keymapper/actions/ActionId.kt | 4 ++++ .../keymapper/actions/ActionUiHelper.kt | 6 ++++++ .../sds100/keymapper/actions/ActionUtils.kt | 18 +++++++++++++++++ .../keymapper/actions/CreateActionDelegate.kt | 10 ++++++++++ .../actions/PerformActionsUseCase.kt | 16 +++++++++++++++ .../system/media/AndroidMediaAdapter.kt | 16 +++++++++++++++ .../keymapper/system/media/MediaAdapter.kt | 2 ++ app/src/main/res/values/strings.xml | 8 ++++++++ 11 files changed, 115 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 187276ce10..237cac90f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #699 Time constraints ⏰ - #257 Action to interact with user interface elements inside other apps. +- #1663 Actions to stop, step forward, and step backward playing media. ## Changed diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index cb9ea44868..56dfd47265 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -370,6 +370,16 @@ sealed class ActionData : Comparable { data class Stop(override val packageName: String) : ControlMediaForApp() { override val id = ActionId.STOP_MEDIA_PACKAGE } + + @Serializable + data class StepForward(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STEP_FORWARD_PACKAGE + } + + @Serializable + data class StepBackward(override val packageName: String) : ControlMediaForApp() { + override val id = ActionId.STEP_BACKWARD_PACKAGE + } } @Serializable @@ -413,6 +423,16 @@ sealed class ActionData : Comparable { data object Stop : ControlMedia() { override val id = ActionId.STOP_MEDIA } + + @Serializable + data object StepForward : ControlMedia() { + override val id = ActionId.STEP_FORWARD + } + + @Serializable + data object StepBackward : ControlMedia() { + override val id = ActionId.STEP_BACKWARD + } } @Serializable diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index fc60df2b62..8eedce9b51 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -357,6 +357,8 @@ object ActionDataEntityMapper { ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, ActionId.STOP_MEDIA_PACKAGE, + ActionId.STEP_FORWARD_PACKAGE, + ActionId.STEP_BACKWARD_PACKAGE, -> { val packageName = entity.extras.getData(ActionEntity.EXTRA_PACKAGE_NAME).valueOrNull() @@ -387,6 +389,12 @@ object ActionDataEntityMapper { ActionId.STOP_MEDIA_PACKAGE -> ActionData.ControlMediaForApp.Stop(packageName) + ActionId.STEP_FORWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepForward(packageName) + + ActionId.STEP_BACKWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepBackward(packageName) + else -> throw Exception("don't know how to create system action for $actionId") } } @@ -466,6 +474,8 @@ object ActionDataEntityMapper { ActionId.FAST_FORWARD -> ActionData.ControlMedia.FastForward ActionId.REWIND -> ActionData.ControlMedia.Rewind ActionId.STOP_MEDIA -> ActionData.ControlMedia.Stop + ActionId.STEP_FORWARD -> ActionData.ControlMedia.StepForward + ActionId.STEP_BACKWARD -> ActionData.ControlMedia.StepBackward ActionId.GO_BACK -> ActionData.GoBack ActionId.GO_HOME -> ActionData.GoHome @@ -994,6 +1004,10 @@ object ActionDataEntityMapper { ActionId.REWIND_PACKAGE to "rewind_package", ActionId.STOP_MEDIA to "stop_media", ActionId.STOP_MEDIA_PACKAGE to "stop_media_package", + ActionId.STEP_FORWARD to "step_forward", + ActionId.STEP_FORWARD_PACKAGE to "step_forward_package", + ActionId.STEP_BACKWARD to "step_backward", + ActionId.STEP_BACKWARD_PACKAGE to "step_backward_package", ActionId.GO_BACK to "go_back", ActionId.GO_HOME to "go_home", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt index e1fa780d7f..5b93ee03a7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt @@ -81,6 +81,10 @@ enum class ActionId { REWIND_PACKAGE, STOP_MEDIA, STOP_MEDIA_PACKAGE, + STEP_FORWARD, + STEP_FORWARD_PACKAGE, + STEP_BACKWARD, + STEP_BACKWARD_PACKAGE, GO_BACK, GO_HOME, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index ea942177b8..08c7b3c5c9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -223,6 +223,8 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package_formatted + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package_formatted + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package_formatted } getString(resId, appName) @@ -237,6 +239,8 @@ class ActionUiHelper( is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package } getString(resId) @@ -456,6 +460,8 @@ class ActionUiHelper( ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) ActionData.ControlMedia.Stop -> getString(R.string.action_stop_media) + ActionData.ControlMedia.StepForward -> getString(R.string.action_step_forward_media) + ActionData.ControlMedia.StepBackward -> getString(R.string.action_step_backward_media) ActionData.CopyText -> getString(R.string.action_text_copy) ActionData.CutText -> getString(R.string.action_text_cut) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index 55073a03fc..7cff7aeb8d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -30,6 +30,7 @@ import androidx.compose.material.icons.outlined.FastForward import androidx.compose.material.icons.outlined.FastRewind import androidx.compose.material.icons.outlined.FlashlightOff import androidx.compose.material.icons.outlined.FlashlightOn +import androidx.compose.material.icons.outlined.Forward30 import androidx.compose.material.icons.outlined.Fullscreen import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Http @@ -45,6 +46,7 @@ import androidx.compose.material.icons.outlined.PhonelinkRing import androidx.compose.material.icons.outlined.Pinch import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.PowerSettingsNew +import androidx.compose.material.icons.outlined.Replay30 import androidx.compose.material.icons.outlined.ScreenLockRotation import androidx.compose.material.icons.outlined.ScreenRotation import androidx.compose.material.icons.outlined.Settings @@ -185,6 +187,10 @@ object ActionUtils { ActionId.REWIND_PACKAGE -> ActionCategory.MEDIA ActionId.STOP_MEDIA -> ActionCategory.MEDIA ActionId.STOP_MEDIA_PACKAGE -> ActionCategory.MEDIA + ActionId.STEP_FORWARD -> ActionCategory.MEDIA + ActionId.STEP_FORWARD_PACKAGE -> ActionCategory.MEDIA + ActionId.STEP_BACKWARD -> ActionCategory.MEDIA + ActionId.STEP_BACKWARD_PACKAGE -> ActionCategory.MEDIA ActionId.GO_BACK -> ActionCategory.NAVIGATION ActionId.GO_HOME -> ActionCategory.NAVIGATION @@ -296,6 +302,10 @@ object ActionUtils { ActionId.REWIND_PACKAGE -> R.string.action_rewind_package ActionId.STOP_MEDIA -> R.string.action_stop_media ActionId.STOP_MEDIA_PACKAGE -> R.string.action_stop_media_package + ActionId.STEP_FORWARD -> R.string.action_step_forward_media + ActionId.STEP_FORWARD_PACKAGE -> R.string.action_step_forward_media_package + ActionId.STEP_BACKWARD -> R.string.action_step_backward_media + ActionId.STEP_BACKWARD_PACKAGE -> R.string.action_step_backward_media_package ActionId.GO_BACK -> R.string.action_go_back ActionId.GO_HOME -> R.string.action_go_home ActionId.OPEN_RECENTS -> R.string.action_open_recents @@ -411,6 +421,10 @@ object ActionUtils { ActionId.REWIND_PACKAGE -> R.drawable.ic_outline_fast_rewind_24 ActionId.STOP_MEDIA -> R.drawable.ic_outline_pause_24 ActionId.STOP_MEDIA_PACKAGE -> R.drawable.ic_outline_pause_24 + ActionId.STEP_FORWARD -> null + ActionId.STEP_FORWARD_PACKAGE -> null + ActionId.STEP_BACKWARD -> null + ActionId.STEP_BACKWARD_PACKAGE -> null ActionId.GO_BACK -> R.drawable.ic_baseline_arrow_back_24 ActionId.GO_HOME -> R.drawable.ic_outline_home_24 ActionId.OPEN_RECENTS -> null @@ -730,6 +744,10 @@ object ActionUtils { ActionId.REWIND_PACKAGE -> Icons.Outlined.FastRewind ActionId.STOP_MEDIA -> Icons.Outlined.StopCircle ActionId.STOP_MEDIA_PACKAGE -> Icons.Outlined.StopCircle + ActionId.STEP_FORWARD -> Icons.Outlined.Forward30 + ActionId.STEP_FORWARD_PACKAGE -> Icons.Outlined.Forward30 + ActionId.STEP_BACKWARD -> Icons.Outlined.Replay30 + ActionId.STEP_BACKWARD_PACKAGE -> Icons.Outlined.Replay30 ActionId.GO_BACK -> Icons.AutoMirrored.Outlined.ArrowBack ActionId.GO_HOME -> Icons.Outlined.Home ActionId.OPEN_RECENTS -> Icons.Outlined.ViewArray diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index bad924c888..0e1a52efe1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -182,6 +182,8 @@ class CreateActionDelegate( ActionId.FAST_FORWARD_PACKAGE, ActionId.REWIND_PACKAGE, ActionId.STOP_MEDIA_PACKAGE, + ActionId.STEP_FORWARD_PACKAGE, + ActionId.STEP_BACKWARD_PACKAGE, -> { val packageName = navigate( @@ -215,6 +217,12 @@ class CreateActionDelegate( ActionId.STOP_MEDIA_PACKAGE -> ActionData.ControlMediaForApp.Stop(packageName) + ActionId.STEP_FORWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepForward(packageName) + + ActionId.STEP_BACKWARD_PACKAGE -> + ActionData.ControlMediaForApp.StepBackward(packageName) + else -> throw Exception("don't know how to create action for $actionId") } @@ -734,6 +742,8 @@ class CreateActionDelegate( ActionId.FAST_FORWARD -> return ActionData.ControlMedia.FastForward ActionId.REWIND -> return ActionData.ControlMedia.Rewind ActionId.STOP_MEDIA -> return ActionData.ControlMedia.Stop + ActionId.STEP_FORWARD -> return ActionData.ControlMedia.StepForward + ActionId.STEP_BACKWARD -> return ActionData.ControlMedia.StepBackward ActionId.GO_BACK -> return ActionData.GoBack ActionId.GO_HOME -> return ActionData.GoHome diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 551a3b5154..80be40ea79 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -227,6 +227,14 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.stop(action.packageName) } + is ActionData.ControlMediaForApp.StepForward -> { + result = mediaAdapter.stepForward(action.packageName) + } + + is ActionData.ControlMediaForApp.StepBackward -> { + result = mediaAdapter.stepBackward(action.packageName) + } + is ActionData.Rotation.CycleRotations -> { result = displayAdapter.disableAutoRotate().then { val currentOrientation = displayAdapter.cachedOrientation @@ -555,6 +563,14 @@ class PerformActionsUseCaseImpl( result = mediaAdapter.stop() } + is ActionData.ControlMedia.StepForward -> { + result = mediaAdapter.stepForward() + } + + is ActionData.ControlMedia.StepBackward -> { + result = mediaAdapter.stepBackward() + } + is ActionData.GoBack -> { result = accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt index 7ca1d7cba6..3886863a38 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/AndroidMediaAdapter.kt @@ -94,6 +94,22 @@ class AndroidMediaAdapter(context: Context, coroutineScope: CoroutineScope) : Me return Success(Unit) } + override fun stepForward(packageName: String?): Result<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_FORWARD, packageName) + } else { + return Error.SdkVersionTooLow(Build.VERSION_CODES.M) + } + } + + override fun stepBackward(packageName: String?): Result<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sendMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD, packageName) + } else { + return Error.SdkVersionTooLow(Build.VERSION_CODES.M) + } + } + override fun getActiveMediaSessionPackages(): List { return activeMediaSessions.value .filter { it.playbackState?.state == PlaybackState.STATE_PLAYING } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt index 4c778078af..c0a4858e81 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/media/MediaAdapter.kt @@ -36,6 +36,8 @@ interface MediaAdapter { fun previousTrack(packageName: String? = null): Result<*> fun nextTrack(packageName: String? = null): Result<*> fun stop(packageName: String? = null): Result<*> + fun stepForward(packageName: String? = null): Result<*> + fun stepBackward(packageName: String? = null): Result<*> fun playFile(uri: String, stream: VolumeStream): Result<*> fun stopFileMedia(): Result<*> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f10c306f79..2dc876b22f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1001,6 +1001,14 @@ Stop media for an app Stop media for %s + Step media forward + Step media forward for an app + Step media forward for %s + + Step media backward + Step media backward for an app + Step media backward for %s + Go back Go home Open recents From f29c29ec8095271870be3ce79c3ed36d1a1b2a4b Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 7 May 2025 23:49:31 +0200 Subject: [PATCH 42/69] fix changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237cac90f7..cebf36aee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,6 @@ - Rename tap screen actions inside key maps. -## Changed - -- Rename tap screen actions inside key maps. - ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 From e1b46ce33c645c29f0f67925f772cdf063519c32 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 12:15:55 +0200 Subject: [PATCH 43/69] #1683 fix: Key event actions do not work in some apps due to missing source field --- CHANGELOG.md | 4 +++ .../io/github/sds100/keymapper/UseCases.kt | 4 +-- .../shizuku/ShizukuInputEventInjector.kt | 14 ++-------- .../system/inputevents/InputEventInjector.kt | 28 +++++++++++++++++++ .../inputmethod/ImeInputEventInjector.kt | 25 ++++------------- app/version.properties | 2 +- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cebf36aee3..32babf419d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Rename tap screen actions inside key maps. +## Bug fixes + +- #1683 key event actions work in Minecraft and other apps again. + ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) #### 28 April 2025 diff --git a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt index caee6e48b0..260a3eca4a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt +++ b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt @@ -142,7 +142,7 @@ object UseCases { ServiceLocator.intentAdapter(ctx), getActionError(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), - ShizukuInputEventInjector(coroutineScope = ServiceLocator.appCoroutineScope(ctx)), + ShizukuInputEventInjector(), ServiceLocator.packageManagerAdapter(ctx), ServiceLocator.appShortcutAdapter(ctx), ServiceLocator.popupMessageAdapter(ctx), @@ -179,7 +179,7 @@ object UseCases { ServiceLocator.audioAdapter(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), service, - ShizukuInputEventInjector(ServiceLocator.appCoroutineScope(ctx)), + ShizukuInputEventInjector(), ServiceLocator.popupMessageAdapter(ctx), ServiceLocator.permissionAdapter(ctx), ServiceLocator.resourceProvider(ctx), diff --git a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt index 9608b58f1c..2b812d21c0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt @@ -8,7 +8,6 @@ import android.view.KeyEvent import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.util.InputEventType -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import rikka.shizuku.ShizukuBinderWrapper @@ -16,7 +15,7 @@ import rikka.shizuku.SystemServiceHelper import timber.log.Timber @SuppressLint("PrivateApi") -class ShizukuInputEventInjector(private val coroutineScope: CoroutineScope) : InputEventInjector { +class ShizukuInputEventInjector : InputEventInjector { companion object { // private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 @@ -40,16 +39,7 @@ class ShizukuInputEventInjector(private val coroutineScope: CoroutineScope) : In val eventTime = SystemClock.uptimeMillis() - val keyEvent = KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - ) + val keyEvent = createInjectedKeyEvent(eventTime, action, model) withContext(Dispatchers.IO) { // MUST wait for the application to finish processing the event before sending the next one. diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index a126bac316..f2780cc903 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -1,7 +1,35 @@ package io.github.sds100.keymapper.system.inputevents +import android.view.InputDevice +import android.view.KeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel interface InputEventInjector { suspend fun inputKeyEvent(model: InputKeyModel) + + fun createInjectedKeyEvent( + eventTime: Long, + action: Int, + model: InputKeyModel, + ): KeyEvent { + val source = when { + InputEventUtils.isDpadKeyCode(model.keyCode) -> InputDevice.SOURCE_DPAD + KeyEvent.isGamepadButton(model.keyCode) -> InputDevice.SOURCE_GAMEPAD + else -> InputDevice.SOURCE_KEYBOARD + } + + return KeyEvent( + eventTime, + eventTime, + action, + model.keyCode, + model.repeat, + model.metaState, + model.deviceId, + model.scanCode, + 0, + // See issue #1683. Some apps ignore key events which do not have a source. + source, + ) + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt index 756acde8df..e5ce73abdd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt @@ -82,7 +82,7 @@ class ImeInputEventInjectorImpl( val eventTime = SystemClock.uptimeMillis() - val keyEvent = createKeyEvent(eventTime, action, model) + val keyEvent = createInjectedKeyEvent(eventTime, action, model) putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) @@ -90,34 +90,19 @@ class ImeInputEventInjectorImpl( } } - private fun createKeyEvent( - eventTime: Long, - action: Int, - model: InputKeyModel, - ): KeyEvent = KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - ) - private fun inputKeyEventRelayService(model: InputKeyModel, imePackageName: String) { val eventTime = SystemClock.uptimeMillis() when (model.inputType) { InputEventType.DOWN_UP -> { - val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, KeyEventRelayService.CALLBACK_ID_INPUT_METHOD, ) - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, @@ -126,7 +111,7 @@ class ImeInputEventInjectorImpl( } InputEventType.DOWN -> { - val downKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) + val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) keyEventRelayService.sendKeyEvent( downKeyEvent, imePackageName, @@ -135,7 +120,7 @@ class ImeInputEventInjectorImpl( } InputEventType.UP -> { - val upKeyEvent = createKeyEvent(eventTime, KeyEvent.ACTION_UP, model) + val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) keyEventRelayService.sendKeyEvent( upKeyEvent, imePackageName, diff --git a/app/version.properties b/app/version.properties index a9644d9a6e..dad6c30b8f 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=109 +VERSION_CODE=110 VERSION_NUM=0 \ No newline at end of file From ac9dc914ebe1cc4957f9dc8168c9fea1554139f2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 12:21:03 +0200 Subject: [PATCH 44/69] chore: update whats new --- app/src/main/assets/whats-new.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 791c6929e6..d1de1ca498 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -2,6 +2,8 @@ 🔎 Action to interact with app elements. +Fix for Minecraft 1.21.80. + == 3.0 features == 🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. From 7b36280fa9ccd3a99c909c0d8c5bac7544c248f2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 12:21:50 +0200 Subject: [PATCH 45/69] fix style --- .../java/io/github/sds100/keymapper/actions/ActionListItem.kt | 2 +- .../java/io/github/sds100/keymapper/actions/ActionsScreen.kt | 4 ++-- .../keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt | 2 +- .../keymapper/mappings/keymaps/trigger/TriggerScreen.kt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt index 22152922a2..92276cfeb8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionListItem.kt @@ -44,10 +44,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.util.ui.compose.DragDropState import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DragDropState @Composable fun ActionListItem( diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt index 50acaf1326..6e4126b876 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt @@ -39,14 +39,14 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.util.ui.compose.DraggableItem -import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DraggableItem +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import kotlinx.coroutines.flow.update @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt index f93f20f4ef..4b44d2ea23 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt @@ -42,10 +42,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.util.ui.compose.DragDropState import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.FingerprintGestureType import io.github.sds100.keymapper.util.ui.LinkType +import io.github.sds100.keymapper.util.ui.compose.DragDropState @Composable fun TriggerKeyListItem( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt index aa778801b6..f20a2ad284 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerScreen.kt @@ -39,15 +39,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowHeightSizeClass import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme -import io.github.sds100.keymapper.util.ui.compose.DraggableItem -import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState import io.github.sds100.keymapper.mappings.ClickType import io.github.sds100.keymapper.mappings.keymaps.ShortcutModel import io.github.sds100.keymapper.mappings.keymaps.ShortcutRow import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.LinkType import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.DraggableItem import io.github.sds100.keymapper.util.ui.compose.RadioButtonText +import io.github.sds100.keymapper.util.ui.compose.rememberDragDropState @OptIn(ExperimentalMaterial3Api::class) @Composable From 72f0cd884849ac0ba2555bf08544057ae9ac723d Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 13:10:20 +0200 Subject: [PATCH 46/69] #1683 when reinjecting/imitating a key event set the same source as the real key event --- .../actions/PerformActionsUseCase.kt | 9 +++++ .../keymaps/detection/DetectKeyMapsUseCase.kt | 4 ++ .../DetectScreenOffKeyEventsController.kt | 3 ++ .../detection/DpadMotionEventTracker.kt | 2 + .../accessibility/MyAccessibilityService.kt | 2 + .../system/inputevents/InputEventInjector.kt | 10 +---- .../system/inputevents/InputEventUtils.kt | 39 +++++++++++++++++++ .../system/inputevents/MyKeyEvent.kt | 1 + .../system/inputmethod/InputKeyModel.kt | 2 + .../actions/PerformActionsUseCaseTest.kt | 6 +++ .../keymaps/DpadMotionEventTrackerTest.kt | 5 +++ .../mappings/keymaps/KeyMapControllerTest.kt | 9 +++++ 12 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 80be40ea79..fdfb55b8de 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.actions import android.accessibilityservice.AccessibilityService import android.os.Build +import android.view.InputDevice import android.view.KeyEvent import android.view.accessibility.AccessibilityNodeInfo import io.github.sds100.keymapper.R @@ -152,11 +153,19 @@ class PerformActionsUseCaseImpl( is ActionData.InputKeyEvent -> { val deviceId: Int = getDeviceIdForKeyEventAction(action) + // See issue #1683. Some apps ignore key events which do not have a source. + val source = when { + InputEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD + InputEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD + else -> InputDevice.SOURCE_KEYBOARD + } + val model = InputKeyModel( keyCode = action.keyCode, inputType = inputEventType, metaState = keyMetaState.withFlag(action.metaState), deviceId = deviceId, + source = source, ) result = when { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt index fafed46f55..b7db65f9c4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.mappings.keymaps.detection import android.accessibilityservice.AccessibilityService import android.os.SystemClock +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.R import io.github.sds100.keymapper.constraints.ConstraintState @@ -193,6 +194,7 @@ class DetectKeyMapsUseCaseImpl( deviceId: Int, inputEventType: InputEventType, scanCode: Int, + source: Int, ) { val model = InputKeyModel( keyCode, @@ -200,6 +202,7 @@ class DetectKeyMapsUseCaseImpl( metaState, deviceId, scanCode, + source = source, ) if (permissionAdapter.isGranted(Permission.SHIZUKU)) { @@ -258,6 +261,7 @@ interface DetectKeyMapsUseCase { deviceId: Int = 0, inputEventType: InputEventType = InputEventType.DOWN_UP, scanCode: Int = 0, + source: Int = InputDevice.SOURCE_UNKNOWN, ) val isScreenOn: Flow diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt index beb9f826eb..600e9fab2f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectScreenOffKeyEventsController.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.mappings.keymaps.detection +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.devices.InputDeviceInfo @@ -90,6 +91,7 @@ class DetectScreenOffKeyEventsController( scanCode = 0, metaState = 0, repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ), ) } @@ -103,6 +105,7 @@ class DetectScreenOffKeyEventsController( scanCode = 0, metaState = 0, repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt index 202ba121ad..0266b50cfa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DpadMotionEventTracker.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.mappings.keymaps.detection +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputevents.InputEventUtils @@ -107,6 +108,7 @@ class DpadMotionEventTracker { scanCode = 0, device = event.device, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index 99205867b6..c25ce900ec 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -140,6 +140,7 @@ class MyAccessibilityService : scanCode = event.scanCode, device = device, repeatCount = event.repeatCount, + source = event.source, ), ) } @@ -315,6 +316,7 @@ class MyAccessibilityService : scanCode = event.scanCode, device = device, repeatCount = event.repeatCount, + source = event.source, ), KeyEventDetectionSource.ACCESSIBILITY_SERVICE, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index f2780cc903..5a680f9893 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.system.inputevents -import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.inputmethod.InputKeyModel @@ -12,12 +11,6 @@ interface InputEventInjector { action: Int, model: InputKeyModel, ): KeyEvent { - val source = when { - InputEventUtils.isDpadKeyCode(model.keyCode) -> InputDevice.SOURCE_DPAD - KeyEvent.isGamepadButton(model.keyCode) -> InputDevice.SOURCE_GAMEPAD - else -> InputDevice.SOURCE_KEYBOARD - } - return KeyEvent( eventTime, eventTime, @@ -28,8 +21,7 @@ interface InputEventInjector { model.deviceId, model.scanCode, 0, - // See issue #1683. Some apps ignore key events which do not have a source. - source, + model.source, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt index d5b336eabc..a42c1949fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt @@ -830,4 +830,43 @@ object InputEventUtils { fun isDpadDevice(event: InputEvent): Boolean = // Check that input comes from a device with directional pads. event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD + + fun isGamepadButton(keyCode: Int): Boolean { + return when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_BUTTON_MODE, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + -> true + + else -> false + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt index 64384acaac..6517eefa26 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt @@ -9,4 +9,5 @@ data class MyKeyEvent( val scanCode: Int, val device: InputDeviceInfo?, val repeatCount: Int, + val source: Int, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt index ec2942ec0e..17e4a53871 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.system.inputmethod +import android.view.InputDevice import io.github.sds100.keymapper.util.InputEventType /** @@ -12,4 +13,5 @@ data class InputKeyModel( val deviceId: Int = 0, val scanCode: Int = 0, val repeat: Int = 0, + val source: Int = InputDevice.SOURCE_UNKNOWN, ) diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt index b338b65d2c..69b71487f1 100644 --- a/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/actions/PerformActionsUseCaseTest.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.actions +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter @@ -142,6 +143,7 @@ class PerformActionsUseCaseTest { deviceId = fakeGamePad.id, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -171,6 +173,7 @@ class PerformActionsUseCaseTest { deviceId = 0, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -220,6 +223,7 @@ class PerformActionsUseCaseTest { deviceId = fakeKeyboard.id, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_GAMEPAD, ) verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) @@ -278,6 +282,7 @@ class PerformActionsUseCaseTest { deviceId = 11, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_KEYBOARD, ), ) } @@ -318,6 +323,7 @@ class PerformActionsUseCaseTest { deviceId = 10, scanCode = 0, repeat = 0, + source = InputDevice.SOURCE_KEYBOARD, ), ) } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt index b323630108..492a3d788a 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/DpadMotionEventTrackerTest.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.mappings.keymaps +import android.view.InputDevice import android.view.KeyEvent import io.github.sds100.keymapper.mappings.keymaps.detection.DpadMotionEventTracker import io.github.sds100.keymapper.system.devices.InputDeviceInfo @@ -70,6 +71,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = CONTROLLER_1_DEVICE, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ), ), ) @@ -83,6 +85,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = CONTROLLER_1_DEVICE, repeatCount = 0, + source = InputDevice.SOURCE_DPAD, ), ), ) @@ -277,6 +280,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = device, repeatCount = 0, + source = 0, ) } @@ -288,6 +292,7 @@ class DpadMotionEventTrackerTest { scanCode = 0, device = device, repeatCount = 0, + source = 0, ) } } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt index 149d0d7522..7a0fee13b6 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapControllerTest.kt @@ -1166,6 +1166,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) // If both triggers are detected @@ -1180,6 +1181,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) // If no triggers are detected @@ -1194,6 +1196,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } } @@ -2665,6 +2668,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( @@ -2673,6 +2677,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } } @@ -3068,6 +3073,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } @@ -3099,6 +3105,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) } @@ -3126,6 +3133,7 @@ class KeyMapControllerTest { any(), any(), any(), + any(), ) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) } @@ -4137,6 +4145,7 @@ class KeyMapControllerTest { scanCode = scanCode, device = device, repeatCount = repeatCount, + source = 0, ), ) From 9918cf0ff8e105f2d8e168bbe8d829349f0c4c14 Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 13:10:27 +0200 Subject: [PATCH 47/69] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index dad6c30b8f..773194701f 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=110 +VERSION_CODE=112 VERSION_NUM=0 \ No newline at end of file From d4a9202d4b82e3e23315ff78f20d718c3ad0c320 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 8 May 2025 11:10:54 +0000 Subject: [PATCH 48/69] New Crowdin translations by GitHub Action --- app/src/main/res/values-id/strings.xml | 74 ++ app/src/main/res/values-tr/strings.xml | 1178 +++++++++++++++++ .../metadata/android/ar/full_description.txt | 55 + .../metadata/android/ar/short_description.txt | 1 + fastlane/metadata/android/ar/title.txt | 1 + .../android/cs_CZ/full_description.txt | 55 + .../android/cs_CZ/short_description.txt | 1 + fastlane/metadata/android/cs_CZ/title.txt | 1 + .../android/de_DE/full_description.txt | 55 + .../android/de_DE/short_description.txt | 1 + fastlane/metadata/android/de_DE/title.txt | 1 + .../android/es_ES/full_description.txt | 55 + .../android/es_ES/short_description.txt | 1 + fastlane/metadata/android/es_ES/title.txt | 1 + .../android/fr_FR/full_description.txt | 55 + .../android/fr_FR/short_description.txt | 1 + fastlane/metadata/android/fr_FR/title.txt | 1 + .../android/hu_HU/full_description.txt | 55 + .../android/hu_HU/short_description.txt | 1 + fastlane/metadata/android/hu_HU/title.txt | 1 + .../android/id_ID/full_description.txt | 55 + .../android/id_ID/short_description.txt | 1 + fastlane/metadata/android/id_ID/title.txt | 1 + .../android/ka_GE/full_description.txt | 55 + .../android/ka_GE/short_description.txt | 1 + fastlane/metadata/android/ka_GE/title.txt | 1 + .../android/ko_KR/full_description.txt | 55 + .../android/ko_KR/short_description.txt | 1 + fastlane/metadata/android/ko_KR/title.txt | 1 + .../android/pl_PL/full_description.txt | 55 + .../android/pl_PL/short_description.txt | 1 + fastlane/metadata/android/pl_PL/title.txt | 1 + .../android/pt_BR/full_description.txt | 55 + .../android/pt_BR/short_description.txt | 1 + fastlane/metadata/android/pt_BR/title.txt | 1 + .../android/ru_RU/full_description.txt | 55 + .../android/ru_RU/short_description.txt | 1 + fastlane/metadata/android/ru_RU/title.txt | 1 + .../metadata/android/sk/full_description.txt | 55 + .../metadata/android/sk/short_description.txt | 1 + fastlane/metadata/android/sk/title.txt | 1 + .../android/tr_TR/full_description.txt | 55 + .../android/tr_TR/short_description.txt | 1 + fastlane/metadata/android/tr_TR/title.txt | 1 + .../metadata/android/uk/full_description.txt | 55 + .../metadata/android/uk/short_description.txt | 1 + fastlane/metadata/android/uk/title.txt | 1 + .../metadata/android/vi/full_description.txt | 55 + .../metadata/android/vi/short_description.txt | 1 + fastlane/metadata/android/vi/title.txt | 1 + .../android/zh_CN/full_description.txt | 55 + .../android/zh_CN/short_description.txt | 1 + fastlane/metadata/android/zh_CN/title.txt | 1 + .../android/zh_TW/full_description.txt | 55 + .../android/zh_TW/short_description.txt | 1 + fastlane/metadata/android/zh_TW/title.txt | 1 + 56 files changed, 2278 insertions(+) create mode 100644 app/src/main/res/values-id/strings.xml create mode 100644 fastlane/metadata/android/ar/full_description.txt create mode 100644 fastlane/metadata/android/ar/short_description.txt create mode 100644 fastlane/metadata/android/ar/title.txt create mode 100644 fastlane/metadata/android/cs_CZ/full_description.txt create mode 100644 fastlane/metadata/android/cs_CZ/short_description.txt create mode 100644 fastlane/metadata/android/cs_CZ/title.txt create mode 100644 fastlane/metadata/android/de_DE/full_description.txt create mode 100644 fastlane/metadata/android/de_DE/short_description.txt create mode 100644 fastlane/metadata/android/de_DE/title.txt create mode 100644 fastlane/metadata/android/es_ES/full_description.txt create mode 100644 fastlane/metadata/android/es_ES/short_description.txt create mode 100644 fastlane/metadata/android/es_ES/title.txt create mode 100644 fastlane/metadata/android/fr_FR/full_description.txt create mode 100644 fastlane/metadata/android/fr_FR/short_description.txt create mode 100644 fastlane/metadata/android/fr_FR/title.txt create mode 100644 fastlane/metadata/android/hu_HU/full_description.txt create mode 100644 fastlane/metadata/android/hu_HU/short_description.txt create mode 100644 fastlane/metadata/android/hu_HU/title.txt create mode 100644 fastlane/metadata/android/id_ID/full_description.txt create mode 100644 fastlane/metadata/android/id_ID/short_description.txt create mode 100644 fastlane/metadata/android/id_ID/title.txt create mode 100644 fastlane/metadata/android/ka_GE/full_description.txt create mode 100644 fastlane/metadata/android/ka_GE/short_description.txt create mode 100644 fastlane/metadata/android/ka_GE/title.txt create mode 100644 fastlane/metadata/android/ko_KR/full_description.txt create mode 100644 fastlane/metadata/android/ko_KR/short_description.txt create mode 100644 fastlane/metadata/android/ko_KR/title.txt create mode 100644 fastlane/metadata/android/pl_PL/full_description.txt create mode 100644 fastlane/metadata/android/pl_PL/short_description.txt create mode 100644 fastlane/metadata/android/pl_PL/title.txt create mode 100644 fastlane/metadata/android/pt_BR/full_description.txt create mode 100644 fastlane/metadata/android/pt_BR/short_description.txt create mode 100644 fastlane/metadata/android/pt_BR/title.txt create mode 100644 fastlane/metadata/android/ru_RU/full_description.txt create mode 100644 fastlane/metadata/android/ru_RU/short_description.txt create mode 100644 fastlane/metadata/android/ru_RU/title.txt create mode 100644 fastlane/metadata/android/sk/full_description.txt create mode 100644 fastlane/metadata/android/sk/short_description.txt create mode 100644 fastlane/metadata/android/sk/title.txt create mode 100644 fastlane/metadata/android/tr_TR/full_description.txt create mode 100644 fastlane/metadata/android/tr_TR/short_description.txt create mode 100644 fastlane/metadata/android/tr_TR/title.txt create mode 100644 fastlane/metadata/android/uk/full_description.txt create mode 100644 fastlane/metadata/android/uk/short_description.txt create mode 100644 fastlane/metadata/android/uk/title.txt create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt create mode 100644 fastlane/metadata/android/vi/title.txt create mode 100644 fastlane/metadata/android/zh_CN/full_description.txt create mode 100644 fastlane/metadata/android/zh_CN/short_description.txt create mode 100644 fastlane/metadata/android/zh_CN/title.txt create mode 100644 fastlane/metadata/android/zh_TW/full_description.txt create mode 100644 fastlane/metadata/android/zh_TW/short_description.txt create mode 100644 fastlane/metadata/android/zh_TW/title.txt diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 0000000000..1d669e715e --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1d669e715e..e32a75e124 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,74 +1,1252 @@ + Tuşlarınızı özgür bırakın! + Key Mapper, uygulama dışında olduğunuzda düğme basışlarınızı algılayıp değiştirebilmesi için bir erişilebilirlik servisine ihtiyaç duyar. Tuş eşlemeleriniz, erişilebilirlik servisini etkinleştirdiğinizde çalışır. Tetikleyici oluşturmak ve eylemleri test etmek için de bu servisin açık olması gerekir. + seçildi + Etkinleştir + ¯\\_(ツ)_/¯\n\nBurada hiçbir şey yok! + İlk adım, tuş eşlemesini tetikleyecek bazı düğmeler eklemektir.\n\nÖnce ‘Tetikleyici Kaydet’ seçeneğine dokunun ve ardından yeniden eşlemek istediğiniz düğmelere basın. Bunlar burada görünecek.\n\nAlternatif olarak, bir tuş eşlemesini ‘gelişmiş tetikleyici’ kullanarak tetikleyebilirsiniz.\n\nİstediğiniz tuşları karıştırıp eşleştirebilirsiniz! + Root gerektirir + Bas… + Eylem yok + Tetikleyici yok + Bilinmeyen cihaz adı + Açık + Kapalı + Sistemi takip et + Bu cihaz + Herhangi bir cihaz + Varsayılan + Erişilebilirlik servisini etkinleştir + Erişilebilirlik servisini yeniden başlat + Paylaş + Burada hiçbir şey yok! + Burada hiçbir şey yok! + Tekrarlamayı durdur… + Tetikleyici bırakıldığında + Tetikleyici tekrar basıldığında + Sınır ulaşıldığında + Tetikleyici bırakıldığında + Tetikleyici tekrar basıldığında + Gizli uygulamaları göster + Değiştiriciler + ÖNEMLİ!!! Bu koordinatlar yalnızca ekranınız ekran görüntüsüyle aynı yönde olduğunda doğrudur! Bu eylem, ekranda yaptığınız tüm dokunma veya hareketleri iptal eder.\n\nEkranınızdaki bir noktanın koordinatlarını bulmakta yardıma ihtiyacınız varsa, bir ekran görüntüsü alın ve ardından bu eylemin basmasını istediğiniz yere ekran görüntüsüne dokunun. + Not: \"İçeri sıkıştırma\" kullanırken X ve Y BİTİŞ koordinatlarıdır, \"Dışarı sıkıştırma\" kullanırken X ve Y BAŞLANGIÇ koordinatlarıdır. + Eylemleri düzeltmek için dokunun! + Kısıtlamaları düzeltmek için dokunun! + Eylemleri gerçekleştir + Tetikleyici şu olana kadar basılı tut… + Cihaz yok + ¯\\_(ツ)_/¯\n\nEkstra yok! + Tuş olayı eylemini yapılandırma tamamlandı + Koordinat seçimi tamamlandı + Key Mapper günlüğü + Neler Yeni + Ses dosyası, Key Mapper’ın özel veri klasörüne kopyalanacak; bu, dosya taşınsa veya silinse bile eylemlerinizin çalışmaya devam edeceği anlamına gelir. Ayrıca, tuş eşlemelerinizle birlikte zip klasöründe yedeklenecektir. + Kaydedilen ses dosyalarını ayarlardan silebilirsiniz. + Eşleştirilmiş cihaz bulunamadı. Bluetooth açık mı? + Kısayol olarak kullanmak için bir tuş eşlemesine dokunun. + Tuş eşleme kısayolu oluştur + Etkin + Devre dışı + Sıfırla + Cihaz yöneticisini etkinleştirdikten sonra, Key Mapper’ı kaldırmak istiyorsanız bunu DEVRE DIŞI BIRAKMANIZ gerekir. + %sms bekle + Etkinlik başlat: %s + Servis başlat: %s + Yayın gönder: %s + Tuş eşleme kimliği + Kabuğu kullan (Yalnızca ROOT) + Rahatsız Etmeyin modunda düzgün çalışması için izin gerekli! + Ekran kapalıyken tetikleme seçeneği, çalışması için root izni gerektirir! + Bu tetikleyici, telefon çalarken veya görüşme sırasında çalışmaz! + Android, telefonunuz çalarken veya bir görüşme sırasında erişilebilirlik servislerinin ses düğmesi basışlarını algılamasına izin vermez, ancak giriş yöntemi servisleri bunu algılayabilir. Bu nedenle, bu tetikleyicinin çalışmasını istiyorsanız Key Mapper klavyelerinden birini kullanmalısınız. + Android sınırlamaları nedeniyle harekette çok fazla parmak var. + Android sınırlamaları nedeniyle hareket süresi çok yüksek. + DPAD tetikleyicilerinin çalışması için bir Key Mapper klavyesi kullanmalısınız! + Tuş eşlemeleriniz rastgele duracak! + Tuş eşlemeleriniz duraklatıldı! + Devam ettir + Tuş eşlemelerinizin çalışması için erişilebilirlik servisinin açık olması gerekir! + Telefonunuz, Key Mapper’ı arka planda çalışırken kapattı veya çöktü! + Erişilebilirlik servisi etkin! Tuş eşlemeleriniz çalışmalı. + Ekstra günlük kaydı açık! Bir sorunu düzeltmeye çalışmıyorsanız bunu kapatın. + Kapat + Hakkında + %s uygulamasını aç + ‘%s’ yaz + %s%s gir + Kabuk üzerinden %s gir + %s cihazından %s%s gir + %s aç + Ekrana dokun (%d, %d) + Ekrana dokun (%s) + %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır + %d parmakla %d/%d koordinatlarından %d/%d koordinatlarına %dms içinde kaydır (%s) + %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde + %s, %d parmakla %d/%d koordinatlarında %dpx sıkıştırma mesafesiyle %dms içinde (%s) + %s numarayı ara + Ses çal: %s + Seçenekler: + Eylemler: + Tetikleyici: + Kısıtlamalar: + Yukarı kaydır + Aşağı kaydır + Sola kaydır + Sağa kaydır + Ekstralar + Başlangıç X + Başlangıç Y + Bitiş X + Bitiş Y + Sıkıştırma mesafesi (px) + Sıkıştırma türü + İçeri sıkıştırma + Dışarı sıkıştırma + Tuş kodu + Cihazdan + Kısayol adı + Koordinat açıklaması (isteğe bağlı) + Girilecek metin + Açılacak URL + Aranacak telefon numarası + Eylem + Kategoriler + Veri + Paket + Sınıf + Ad + Değer (%s) + Key Mapper için açıklama (gerekli) + Bayraklar + Ses dosyası açıklaması + WiFi ağ SSID’si + Aynı anda + Sırayla + VE + VEYA + Kısa basış + Uzun basış + Çift basış + Doğru + Yanlış + Etkinlik + Servis + Yayın alıcısı + Tetikleyici ve eylemler + Kısıtlamalar ve daha fazlası + Tetikleyici + Eylemler + Kısıtlamalar + Seçenekler + %s seçildi + Yedekleme başarılı! + Yedekleme başarısız! + Geri yükleme başarılı! + Geri yükleme başarısız! + Otomatik yedekleme başarılı! + Otomatik yedekleme başarısız! + Ekran görüntüsü alındı + Ekran görüntüsü çözünürlüğü bu cihazın çözünürlüğüyle eşleşmiyor! + Tuş eşleme UUID’si panoya kopyalandı + Bir tuş eşlemesini tetiklediniz + Günlük kopyalandı + Kaydedilmiş ses dosyanız yok! + Key Mapper, Shizuku kullanarak kendine WRITE_SECURE_SETTINGS izni verdi + Key Mapper, Root kullanarak kendine WRITE_SECURE_SETTINGS izni verdi + Sıra tetikleyici zaman aşımı + Uzun basış gecikmesi + Çift basış zaman aşımı + Tekrarlama gecikmesi + Tekrarlama sınırı + Her… tekrar et + Titreşim süresi + Kaç kez + Her tekrar için kaç kez + Sonraki eylemden önceki gecikme + Basılı tutma süresi + Kaydırma süresi (ms) + Parmak sayısı + Ekran görüntüsüyle ayarlanacak koordinatlar + Başlangıç + Bitiş + Sıkıştırma süresi (ms) + Parmak sayısı + %s ön planda + %s ön planda değil + %s medya oynatıyor + %s medya oynatmıyor + %s bağlı + %s bağlantısı kesildi + Ekran açık + Ekran kapalı + Flaş kapalı + Flaş açık + Ön flaş kapalı + Ön flaş açık + VE + VEYA + Ön plandaki uygulama + Ön planda olmayan uygulama + Bluetooth cihazı bağlı + Bluetooth cihazı bağlantısı kesildi + Ekran açık + Ekran kapalı + Dikey (0°) + Yatay (90°) + Dikey (180°) + Yatay (270°) + Dikey (herhangi) + Yatay (herhangi) + Medya oynatan uygulama + Medya oynatmayan uygulama + Medya oynuyor + Medya oynatmıyor + Flaş açık + Flaş kapalı + WiFi açık + WiFi kapalı + Bir WiFi ağına bağlı + Bir WiFi ağından bağlantı kesildi + Android 10 ve daha yeni sürümlerde uygulamaların bilinen WiFi ağlarının listesini sorgulamasına izin verilmez, bu nedenle SSID’yi manuel olarak yazmanız gerekecek. + + Herhangi bir WiFi ağının eşleşmesi gerekiyorsa boş bırakın. + Herhangi biri + %s WiFi’ye bağlı + %s WiFi’den bağlantı kesildi + Herhangi bir WiFi’ye bağlı + Hiçbir WiFi’ye bağlı değil + Giriş yöntemi seçildi + %s seçildi + Giriş yöntemi seçilmedi + %s seçilmedi + Cihaz kilitli + Cihaz kilidi açık + Kilit ekranı gösteriliyor + Kilit ekranı gösterilmiyor + Telefon görüşmesinde + Telefon görüşmesinde değil + Telefon çalıyor + Şarj oluyor + Şarj olmuyor + Dikey (0°) + Yatay (90°) + Dikey (180°) + Yatay (270°) + Zaman + %s ve %s arasındaki süre + Zaman sınırlaması + Başlangıç zamanı + Başlangıç zamanını düzenle + Bitiş zamanı + Bitiş zamanını düzenle + Uzun basış + Çift basış + Uygulama kapatma özelliğini kapat + Telefonunuzdaki tüm uygulama kapatma \"özelliklerini\" nasıl kapatacağınızı gösteren dontkillmyapp.com adresindeki harika kılavuzu takip edin. + + \n\nKılavuzu okuduktan sonra bir sonraki slayta geçmeniz ve erişilebilirlik servisini yeniden başlatmanız gerekecek. + Kılavuzu aç + Erişilebilirlik servisini yeniden başlat + Erişilebilirlik servisi yeniden başlatılmalıdır. Kapatıp açın. + Hata raporu oluştur + \"Rapor oluştur\" seçeneğine dokunarak hata raporunu kaydedeceğiniz bir konum seçin. Bir sonraki slayt, bunu geliştiriciye nasıl göndereceğinizi açıklayacak. + Rapor oluştur + Raporu paylaş + Hata raporunu geliştiriciyle paylaşmanın 2 yolu var. Discord sunucusuna katılabilir veya GitHub’da bir sorun oluşturabilirsiniz. Mesajınıza hata raporunu eklemeyi unutmayın! + Discord + GitHub + Ayarlar + Hakkında + Ara + Yardım + Hata bildir + Giriş yöntemi seçiciyi göster + Kaydet + Geri yükle + Her şeyi yedekle + Dokunarak duraklat + Dokunarak devam ettir + Kaydet + Kısa mesajları aç/kapat + Kopyala + Temizle + Eylem ekle + Tetikleyici kaydet + Gelişmiş tetikleyiciler + YENİ! + Tamam + Düzelt + Kaydediyor (%d…) + Kısıtlama ekle + Tuş kodu seç + Ekstra ekle + Başlatıcıda kısayol oluştur + Kısayolu manuel olarak oluştur + Intent kılavuzu + Yardım + Ekran görüntüsü seç (isteğe bağlı) + Etkinlik seç + Bayrakları ayarla + Sınır yok + Ses dosyası seç + Eylemi düzenle + Eylemi değiştir + Root izni gerekli! + Erişilebilirlik ayarları sayfası bulunamadı + Kaydedilmemiş değişiklikler + Kaydedilmemiş değişiklikleriniz var. Bunları iptal ederseniz, düzenlemeleriniz kaybolacak. + Telefonunuzun rootlu olmadığını biliyorsanız veya rootun ne olduğunu bilmiyorsanız, yalnızca rootlu cihazlarda çalışan özellikleri kullanamazsınız. ‘Tamam’a dokunduğunuzda ayarlara yönlendirileceksiniz. + Ayarlarda, en alta kaydırın ve root özelliklerini/eylemlerini kullanabilmek için ‘Key Mapper root iznine sahip’ seçeneğine dokunun. + WRITE_SECURE_SETTINGS izni ver + Bu izni vermek için bir PC/Mac gereklidir. Çevrimiçi kılavuzu okuyun. + Cihazınızda erişilebilirlik servisleri ayarları sayfası yok gibi görünüyor. Bunu nasıl düzelteceğinizi açıklayan çevrimiçi kılavuzu okumak için “kılavuz” seçeneğine dokunun. + Tuşlar, basılı tutulacakları sırayla yukarıdan aşağıya listelenmelidir. + Bir “sıra” tetikleyicisi, paralel tetikleyicilerden farklı olarak bir zaman aşımına sahiptir. Bu, ilk tuşa bastıktan sonra tetikleyicideki diğer tuşları girmeniz için belirli bir süreniz olacağı anlamına gelir. Tetikleyiciye eklediğiniz tüm tuşlar, zaman aşımı süresine ulaşılana kadar normal işlevlerini yerine getirmez. Bu zaman aşımını “Seçenekler” sekmesinden değiştirebilirsiniz. + Android, uygulamaların bağlı (eşleştirilmemiş) Bluetooth cihazlarının listesini almasına izin vermez. Uygulamalar yalnızca bu cihazların bağlandığını ve bağlantısının kesildiğini algılayabilir. Bu nedenle, Bluetooth cihazınız erişilebilirlik servisi başladığında zaten bağlıysa, uygulamanın bunu bilmesi için cihazı yeniden bağlamanız gerekecek. + Konumu değiştir veya otomatik yedeklemeyi kapat? + Ekran açma/kapama kısıtlamaları, yalnızca “ekran kapalıyken tetikleyiciyi algıla” tuş eşleme seçeneğini açtıysanız çalışır. Bu seçenek, bazı tuşlar (örneğin ses düğmeleri) için ve yalnızca rootluysanız görünür. Desteklenen tuşların listesini Yardım sayfasında görebilirsiniz. + PIN veya Desen gibi başka bir ekran kilidiniz varsa endişelenmenize gerek yok. Ancak yalnızca Parola ekran kilidi kullanıyorsanız, Key Mapper Temel Giriş Yöntemi’ni kullanırsanız telefonunuzun kilidini açamazsınız çünkü bu yöntemin bir arayüzü yoktur. Key Mapper’a WRITE_SECURE_SETTINGS izni vererek klavyeyi değiştirmek için bir bildirim gösterebilirsiniz. Bunu nasıl yapacağınızı öğrenmek için ekranın altındaki soru işaretine dokunun. + Eylemler için bir giriş yöntemi gerektiren bir yöntem seçin. Bunu daha sonra ana ekranın alt menüsündeki “Eylemler için klavye seç” seçeneğiyle değiştirebilirsiniz. + Caps Lock tuşunun hala büyük harf kilitlemesini engellemek için klavyenizde “Caps Lock’u kameraya” klavye düzenini seçmeniz gerekir. Bu ayarı cihaz ayarlarınızda -> Diller ve Giriş -> Fiziksel Klavye -> Klavyenize dokunun -> Klavye Düzenlerini Ayarla bölümünden bulabilirsiniz. Bu, Caps Lock tuşunu KEYCODE_CAMERA’ya yeniden eşleyerek Key Mapper’ın bunu doğru şekilde eşlemesini sağlar.\n\nBunu yaptıktan sonra Caps Lock tetikleyici tuşunu kaldırıp Caps Lock tuşunu tekrar kaydetmelisiniz. Adımları doğru yaptıysanız “Caps Lock” yerine “Kamera” yazmalıdır. + Bağlı harici cihaz yok. + Key Mapper GUI Klavyesini Yükle + Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin. + Key Mapper Leanback Klavyesini Yükle + Bu şiddetle tavsiye edilir! Bu, Key Mapper ile kullanabileceğiniz Android TV için uygun bir klavyedir. Key Mapper’a dahili olan (Temel Giriş Yöntemi) klavyede ekran klavyesi yoktur. Nereden yüklemek istediğinizi seçin. + Key Mapper GUI Klavyesini Yükle + Nereden indirmek istediğinizi seçin. + Key Mapper Leanback Klavyesini Yükle + Nereden indirmek istediğinizi seçin. + Bu eylem için ek kurulum gerekiyor + Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır: + + \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir. + + \n\n2. Key Mapper GUI Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız. + + \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur. + Bu eylem için ek kurulum gerekiyor + Bu eylemi kullanmak için cihazınızı ayarlamanın 3 yolu var. Her birinin avantajları ve dezavantajları şunlardır: + + \n\n1. Shizuku’yu indirin (önerilen). Şu anda kullandığınız ekran klavyesini değiştirmeniz gerekmez, ancak cihazınızı her yeniden başlattığınızda bir dakikalık kurulum gerektirir. + + \n\n2. Key Mapper Leanback Klavyesini indirin. Bu, Key Mapper ile kullanabileceğiniz Android TV için optimize edilmiş bir ekran klavyesidir, ancak şu anda kullandığınız klavyeyi (örneğin Gboard) kullanamazsınız. + + \n\n3. Hiçbir şey yapmayın ve dahili Key Mapper klavyesini kullanın. Bu önerilmez çünkü Key Mapper’ı kullandığınızda hiçbir ekran klavyeniz olmaz! Hiçbir avantajı yoktur. + Pil optimizasyonunu devre dışı bırak + HEPSİNİ okumanız ZORUNLU, aksi takdirde ileride sinir bozucu sorunlar yaşarsınız!\n\n“Kısmen düzelt” seçeneğine dokunmak, Android’in uygulamayı arka planda durdurmasını belki engelleyebilir.\n\nBu YETERLİ DEĞİL. MIUI veya Samsung Experience gibi OEM arayüzünüzde başka uygulama kapatma özellikleri olabilir, bu nedenle dontkillmyapp.com’daki çevrimiçi kılavuzu takip ederek Key Mapper için bunları da kapatmalısınız. + Erişilebilirlik servisini kapatıp açarak yeniden başlatın. + Bu tetikleyiciyi kullanmak, cihazınızın ayarlarındaki ekran sabitleme ayarını kullandıktan sonra cihazınızın kilidini açtığınızda siyah bir ekran oluşmasına neden olabilir. Bu, bir yeniden başlatma ile düzeltilebilir. Bu durum tüm cihazlarda olmaz, bu yüzden dikkatli olun ve sorun yaşarsanız ayarı kapatın! + Key Mapper kesintiye uğradı + Key Mapper arka planda çalışmaya çalıştı ancak sistem tarafından durduruldu.\nBu, pil veya bellek optimizasyonu açık olduğunda olabilir.\n\nBunu düzeltmek için çevrimiçi bir kılavuzu takip edebilirsiniz. İşiniz bittiğinde servisi de yeniden başlatmalısınız. + Devam et + Yoksay + Hata raporu oluşturulamadı + Hatayı düzelt + Key Mapper için dosya oluşturmanıza izin veren bir dosya uygulamanız yüklü değil. Lütfen bir dosya yöneticisi yükleyin. + Key Mapper için dosya seçmenize izin veren bir dosya uygulamanız yüklü değil. Lütfen bir dosya yöneticisi yükleyin. + Erişilebilirlik servisi etkinleştirilmeli + @string/accessibility_service_explanation + Rahatsız Etmeyin erişimini ver + Cihazınızın hangi uygulamaların Rahatsız Etmeyin durumunu değiştirebileceğini yönetebileceğiniz ayarlar sayfasına yönlendirileceksiniz. Bu bazı cihazlarda mevcut değildir, bu yüzden listede Key Mapper’ı görmüyorsanız “tekrar gösterme” seçeneğine dokunun. + Bilmenizde fayda var! + Bir tetikleyici tuşunun yanında bu sembolü (⌨) görüyorsanız, algılanması için bir Key Mapper klavyesi KULLANMALISINIZ. Bu, Android’deki bir kısıtlamadır ve yalnızca bazı düğmeler için gereklidir. + Önemli! + Key Mapper GUI Klavyesini, bu Key Mapper sürümüyle uyumlu olacak şekilde güncellemelisiniz. Güncelleme yapana kadar bazı tuş eşlemeleri çalışmayabilir! + Şimdi güncelle + Yoksay + Şuna göre sırala + Öncelikleri ayarlamak için tutamaçları sürükleyin. En üstteki öğe en önemlisidir. Ayrıca herhangi bir öğeye dokunarak sıralama sırasını tersine çevirebilirsiniz. + Örnek: Tuş eşlemelerini öncelikle Eylemlerine göre artan sırayla ve ikincil olarak Tetikleyicilerine göre azalan sırayla sıralamak için Eylemleri birinci sıraya, Tetikleyicileri ikinci sıraya taşıyın. + %1$s için tutamaç + Örnek göster + Tanınmayan tuş kodu + Basılı düğme, giriş sistemi tarafından tanınmadı. Geçmişte Key Mapper bu tür düğmeleri tek bir düğme olarak algılıyordu. Şu anda uygulama, düğmeyi tarama koduna göre ayırt etmeye çalışıyor; bu, daha benzersiz olmalıdır. Ancak bu, benzersizliği garanti etmeyen geçici ve eksik bir çözümdür. + Tamam + Kılavuz + Kılavuz + Değiştir + Kısmen düzelt + Tamam + Yeniden başlat + Tekrar gösterme + Uygula + Değişiklikleri iptal et + Kaydet + Anladım + Kapat + İptal + Tekrar gösterme + Düzenlemeye devam et + Gizle + Çevrimiçi kılavuz + Ayarlar + Belgeler + Değişiklik günlüğü + Shizuku + Key Mapper GUI Klavyesi + Key Mapper Leanback Klavyesi + Hiçbir şey yapma + Düzelt + Klavye seçici + Tuş eşlemelerini duraklat/devam ettir + Klavye gizli uyarısı + Key Mapper klavyesini aç/kapat + Yeni özellikler + Klavyenizi değiştirmek için dokunun. + Klavye seçici + Çalışıyor + Key Mapper’ı açmak için dokunun. + Duraklat + Duraklatıldı + Key Mapper’ı açmak için dokunun. + Devam ettir + Kapat + Yeniden başlat + Erişilebilirlik servisi devre dışı + Erişilebilirlik servisini başlatmak için dokunun. + Erişilebilirlik servisi yeniden başlatılmalı! + Erişilebilirlik servisi çöktü! Telefonunuz bunu agresif bir şekilde kapatmış olabilir! Erişilebilirlik servisini yeniden başlatmak için dokunun. + Servisi durdur + Klavye gizli! + Klavyeyi tekrar göstermeye başlamak için ‘klavyeyi göster’e dokunun. + Key Mapper klavyesini aç/kapat + Key Mapper klavyesine ve klavyenizden geçiş yapmak için ‘aç/kapat’a dokunun. + Aç/Kapat + Varsayılan uzun basış gecikmesi (ms) + Bir düğmenin uzun basış olarak algılanması için ne kadar süre basılı tutulması gerektiği. Varsayılan 500ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan çift basış süresi (ms) + Bir düğmenin çift basış olarak algılanması için ne kadar hızlı çift basılması gerektiği. Varsayılan 300ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Bir tuş eşlemesi için titreşim etkinse ne kadar süre titreşeceği. Varsayılan 200ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan titreşim süresi (ms) + Eylemin tekrarlanmaya başlaması için tetikleyicinin ne kadar süre basılı tutulması gerektiği. Varsayılan 400ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan tekrar gecikmesi (ms) + Bir eylemin her tekrar arasındaki gecikme. Varsayılan 50ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan tekrarlar arası gecikme (ms) + Bir sıra tetikleyicisini tamamlamak için izin verilen süre. Varsayılan 1000ms’dir. Bir tuş eşlemesinin seçeneklerinde geçersiz kılınabilir. + Varsayılan sıra tetikleyici zaman aşımı (ms) + Sıfırla + Tüm tuş eşlemelerini titreşime zorla. + Titreşimi zorla + Klavye seçici bildirimi + Klavye seçmenize olanak tanıyan kalıcı bir bildirim göster. + Tuş eşlemelerini duraklat/devam ettir bildirimi + Tuş eşlemelerinizi başlatan/duraklatan kalıcı bir bildirim göster. + Tuş eşlemelerini belirtilen bir konuma otomatik olarak yedekle + Konum seçilmedi. + Cihazları seç + Klavye seçiciyi otomatik olarak göster + Seçtiğiniz bir cihaz bağlandığında veya bağlantısı kesildiğinde klavye seçici otomatik olarak gösterilir. Aşağıdan cihazları seçin. + Bir cihaz (örneğin klavye) bağlandığında/bağlantısı kesildiğinde ekran klavyesini otomatik olarak değiştir + Seçilen bir cihaz bağlandığında son kullanılan Key Mapper klavyesi otomatik olarak seçilir. Cihazın bağlantısı kesildiğinde normal klavyeniz otomatik olarak seçilir. + Metin girmeye başladığınızda ekran klavyesini otomatik olarak değiştir + Klavyeyi açmaya çalıştığınızda son kullanılan Key Mapper dışı klavye otomatik olarak seçilir. Klavyeyi kullanmayı bıraktığınızda Key Mapper klavyeniz otomatik olarak seçilir. + Klavyeyi otomatik olarak değiştirirken ekranda bir mesaj göster + Key Mapper root iznine sahip + Yalnızca rootlu cihazlarda çalışan özellikleri/eylemleri kullanmak istiyorsanız bunu etkinleştirin. Bu özelliklerin çalışması için Key Mapper’ın root erişim yönetim uygulamanızdan (örneğin Magisk, SuperSU) root izni almış olması gerekir. + Bunu yalnızca cihazınızın rootlu olduğunu biliyor ve Key Mapper’a root izni verdiyseniz açın. + Tema seç + Açık ve koyu temalar mevcut + Bildirime dokunduğunuzda Key Mapper klavyesi ile varsayılan klavyeniz arasında geçiş yapın. + Key Mapper klavyesini aç/kapat bildirimi + Tuş eşlemelerini açarken/kapatırken klavyeyi otomatik olarak değiştir + Tuş eşlemelerinizi devam ettirdiğinizde Key Mapper klavyesini, duraklattığınızda ise varsayılan klavyenizi otomatik olarak seçin. + Ana ekran uyarılarını gizle + Ana ekranın üstündeki uyarıları gizle. + Cihaza özgü tetikleyiciler için cihaz kimliğinin ilk 5 karakterini göster + Bu, aynı ada sahip cihazları ayırt etmek için kullanışlıdır. + ABD İngilizcesine ayarlanmış klavyeleri düzelt + Bu, bir erişilebilirlik servisi etkinleştirildiğinde doğru klavye düzenine sahip olmayan klavyeleri düzeltir. Daha fazla bilgi okuyup yapılandırmak için dokunun. + ABD İngilizcesine ayarlanmış klavyeleri düzelt + Android 11’de bir hata var; bir erişilebilirlik servisi açıldığında Android, tüm harici cihazları aynı dahili sanal cihaz olarak görüyor. Bu cihazları doğru şekilde tanımlayamadığı için hangi klavye düzenini kullanacağını bilemiyor ve örneğin bir Alman klavyesi olsa bile varsayılan olarak ABD İngilizcesini kullanıyor. Aşağıdaki adımları izleyerek Key Mapper ile bu sorunu çözebilirsiniz. + 4. Cihazları seç + 1. Key Mapper GUI Klavyesini yükle (isteğe bağlı) + 1. Key Mapper Leanback Klavyesini yükle (isteğe bağlı) + 2. Key Mapper GUI Klavyesini veya Key Mapper Temel Giriş Yöntemi’ni etkinleştir + 2. Key Mapper Leanback Klavyesini veya Key Mapper Temel Giriş Yöntemi’ni etkinleştir + 3. Az önce etkinleştirdiğiniz klavyeyi kullan + (Önerilen) Bu ayar için kullanıcı kılavuzunu okuyun. + Ekstra günlüğü etkinleştir + Günlüğü görüntüle ve paylaş + Sorun bildir + Ses dosyalarını sil + Ses eylemi için kullanılabilecek ses dosyalarını sil. + İzin ver + İzin verildi + 1. Shizuku yüklü değil! Shizuku uygulamasını indirmek için dokunun. + 1. Shizuku yüklü. + 2. Shizuku başlatılmadı! Shizuku uygulamasını açmak için dokunun ve ardından nasıl başlatılacağına dair talimatlarını okuyun. + 2. Shizuku başlatıldı. + 3. Key Mapper’ın Shizuku’yu kullanma izni yok. Bu izni vermek için dokunun. + 3. Key Mapper otomatik olarak Shizuku’yu kullanacak. Key Mapper’ın hangi özelliklerinin Shizuku’yu kullandığını okumak için dokunun. + Varsayılan eşleme seçenekleri + Tuş eşlemeleriniz için varsayılan seçenekleri değiştirin. + Tüm ayarları sıfırla + TEHLİKE! Uygulamadaki tüm ayarları varsayılana sıfırla. Tuş eşlemeleriniz sıfırlanMAYACAK. + TEHLİKE! + Uygulamadaki tüm ayarları varsayılana sıfırlamak istediğinizden emin misiniz? Tuş eşlemeleriniz sıfırlanMAYACAK. Giriş ekranı ve tüm uyarı pop-up’ları tekrar görünecek. + Evet, sıfırla + Klavye seçiciyi otomatik olarak göster + Klavye seçiciyi otomatik olarak göstermeye olanak tanıyan ayarları görmek için dokunun. + Root ayarları + Bu seçenekler yalnızca rootlu cihazlarda çalışır! Rootun ne olduğunu veya cihazınızın rootlu olup olmadığını bilmiyorsanız, bunlar çalışmazsa lütfen kötü bir inceleme bırakmayın. :) + WRITE_SECURE_SETTINGS izni gerektirir + Bu seçenekler yalnızca Key Mapper WRITE_SECURE_SETTINGS iznine sahipse etkinleşir. İzni nasıl vereceğinizi öğrenmek için aşağıdaki düğmeye tıklayın. + Shizuku desteği + Shizuku, Key Mapper’ın yalnızca sistem uygulamalarının yapabileceği şeyleri yapmasını sağlayan bir uygulamadır. Örneğin, Key Mapper klavyesini kullanmanız gerekmez. Bunu nasıl kuracağınızı öğrenmek için dokunun. + Shizuku’yu kurmak için bu adımları izleyin. + Klavyeyi otomatik olarak değiştir + Bunlar gerçekten kullanışlı ayarlar ve kontrol etmeniz önerilir! + Günlük kaydı + Bu, tuş eşlemelerinize gecikme ekleyebilir, bu yüzden yalnızca uygulamayı hata ayıklamaya çalışıyorsanız veya geliştirici tarafından istenmişse açın. + Ses diyaloğunu göster + Titre + Ekranda mesaj göster + Uzun basışta tekrar titre + Ekran kapalıyken tetikleyiciyi algıla + Tekrarla + %dx + %dms sonra + her %dms’de + tekrar basılana kadar + bırakılana kadar + Tekrarla + Basılı tut + Tekrar basılana kadar basılı tut + Yeniden eşleme yapma + Bu tuş eşlemesini diğer uygulamaların tetiklemesine izin ver + Tuş eşleme kimliğini kopyala + + Erişilebilirlik + Alarm + DTMF + Müzik + Bildirimler + Zil + Sistem + Sesli arama + Normal + Titreşim + Sessiz + Ön + Arka + Alarmlar + Öncelik + Hiçbir şey + Tuş eşlemelerini duraklat + Tuş eşlemelerini devam ettir + Duraklatıldı + Çalışıyor + Servis Devre Dışı + Key Mapper erişilebilirlik servisi devre dışı + Key Mapper klavyesini aç/kapat + Ctrl + Sol Ctrl + Sağ Ctrl + Alt + Sol Alt + Sağ Alt + Shift + Sol Shift + Sağ Shift + Meta + Sol Meta + Sağ Meta + Sym + Func + Caps Lock + Num Lock + Scroll Lock + Bu eylemin çalışması için Key Mapper klavyelerinden birini kullanıyor olmanız gerekiyor! + %s paket adına sahip uygulama yüklü değil! + %s uygulaması devre dışı! + Key Mapper\'a sistem ayarlarını değiştirme izni vermeniz gerekiyor. + Bu işlem root izni gerektiriyor! + Bu eylem kamera izni gerektiriyor! + Android %s veya daha yeni bir sürüm gerektiriyor + Android %s veya daha eski bir sürüm gerektiriyor + Cihazınızda kamera bulunmuyor. + Cihazınız NFC\'yi desteklemiyor. + Cihazınızda parmak izi okuyucu bulunmuyor. + Cihazınız WiFi\'yi desteklemiyor. + Cihazınız Bluetooth\'u desteklemiyor. + Cihazınız cihaz politikası uygulamasını desteklemiyor. + Cihazınızda kamera flaşı bulunmuyor. + Cihazınızda telefon özellikleri bulunmuyor. + Klavye ayarları sayfası bulunamadı! + Key Mapper\'ın cihaz yöneticisi olması gerekiyor! + Key Mapper\'ın bu kısayolu kullanma izni yok + Uygulamanın Rahatsız Etmeyin durumunu değiştirmek için izne ihtiyacı var! + Bu eylem telefon durumunu okuma izni gerektiriyor! + WRITE_SETTINGS izin sayfası bulunamadı! + Bu uygulama kısayolu açılırken hata oluştu + Rahatsız Etmeyin erişim izni ayarları bulunamadı! + Key Mapper\'ın WRITE_SECURE_SETTINGS iznine ihtiyacı var. + Bu telefon aramasını başlatabilecek bir uygulama yok + Kamera kullanımda! + Kamera bağlantısı kesildi! + Kamera devre dışı! + Kamera hatası! + Maksimum kamera kullanımda! + Ön flaş yok + Arka flaş yok + Değişken flaş ışığı gücü desteklenmiyor + Erişilebilirlik servisinin etkinleştirilmesi gerekiyor! + Erişilebilirlik servisinin yeniden başlatılması gerekiyor! + Başlatıcınız kısayolları desteklemiyor. + Bir Key Mapper klavyesinin etkinleştirilmesi gerekiyor! + %s giriş yöntemi bulunamadı + Giriş yöntemi seçici gösterilemiyor! + Erişilebilirlik düğümü bulunamadı! + %s genel eylemi gerçekleştirilemedi! + Pil optimizasyon ayarları bulunamadı! Varsa, manuel olarak açın. + Ekstra (%s) bulunamadı! + Aynı kısıtlamaya iki kez sahip olamazsınız! + Boş olamaz! + Cihaz bulunamadı! + Boş JSON dosyası! + Dosya erişimi reddedildi! %s + Bilinmeyen G/Ç hatası! + İptal edildi! + Geçersiz numara! + En az %s olmalı! + En fazla %s olmalı! + Pil optimizasyonu açık! Key Mapper\'ın rastgele durmasını engellemek için bunu kapatın. + Bildirim erişim izni reddedildi! + Geçersiz! + Telefon araması başlatma izni reddedildi! + Bu yedeği kullanmak için Key Mapper\'ı en son sürüme güncellemeniz gerekiyor. + Sesli asistan yüklü değil! + Yetersiz izinler + Yalnızca Key Mapper klavyeleri yüklü! + Medya oynatan bir uygulama yok! + Kaynak dosya bulunamadı! %s + Hedef dosya bulunamadı! %s + Hareket girişi başarısız! + Sistem ayarı %s değiştirilemedi! + %s etkinleştirilmeli! + Giriş yöntemi değiştirilemedi! + Cihazınızda kamera uygulaması yok! + Cihazınızda asistan yok! + Cihazınızda ayarlar uygulaması yok! + Bu URL\'yi açabilecek bir uygulama yok! + Klasör değil! %s + Dosya değil! %s + Dizin bulunamadı! %s + Ses dosyası bulunamadı! + Depolama izni reddedildi! + Kaynak ve hedef aynı olamaz! + Hedefte yer kalmadı! %s + Shizuku izni reddedildi! + Shizuku başlatılmadı! + Bu dosyanın adı yok! + Geçersiz dosya. Key Mapper\'dan dışa aktarılmış bir zip olmalı. + Key Mapper\'a eşleştirilmiş Bluetooth cihazlarını görme izni vermelisiniz. + Hatalı URL. http:// kısmını unuttunuz mu? + Hassas konum okuma izni reddedildi! + Telefon aramalarını yanıtlama ve sonlandırma izni reddedildi! + Eşleştirilmiş Bluetooth cihazlarını görme izni reddedildi! + Bildirim gösterme izni reddedildi! + 2 veya daha fazla olmalı! + %d veya daha az olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + 0\'dan büyük olmalı! + %d veya daha az olmalı! + UI öğesi bulunamadı! + WiFi\'yi aç/kapat + WiFi\'yi aç + WiFi\'yi kapat + Bluetooth\'u aç/kapat + Bluetooth\'u aç + Bluetooth\'u kapat + Sesi artır + Sesi azalt + Sesi kapat + Sesi aç/kapat + Sesi aç + Ses kontrol panelini göster + Akışı artır + %s akışını artır + Akışı azalt + %s akışını azalt + Zil modlarını değiştir (Normal, Titreşim, Sessiz) + Zil modlarını değiştir (Normal, Titreşim) + Zil modunu değiştir + %s moduna geç + Rahatsız Etmeyin modunu aç/kapat + Yalnızca %s için Rahatsız Etmeyin modunu aç/kapat + Rahatsız Etmeyin modunu aç + Yalnızca %s için Rahatsız Etmeyin modunu aç + Rahatsız Etmeyin modunu kapat + Otomatik döndürmeyi aç + Otomatik döndürmeyi kapat + Otomatik döndürmeyi aç/kapat + Dikey mod + Yatay mod + Yönü değiştir + Dönüşleri sırayla değiştir + %s dönüşlerini sırayla değiştir + Mobil veriyi aç/kapat + Mobil veriyi aç + Mobil veriyi kapat + Otomatik parlaklığı aç/kapat + Otomatik parlaklığı kapat + Otomatik parlaklığı aç + Ekran parlaklığını artır + Ekran parlaklığını azalt + Bildirim panelini aç + Bildirim panelini aç/kapat + Hızlı ayarları aç + Hızlı ayarlar panelini aç/kapat + Durum çubuğunu kapat + Medya oynatmayı duraklat + Bir uygulama için medya oynatmayı duraklat + %s için medyayı duraklat + Medya oynatmayı devam ettir + Bir uygulama için medya oynatmayı devam ettir + %s için medyayı devam ettir + Medya oynatmayı oynat/duraklat + Bir uygulama için medya oynatmayı oynat/duraklat + %s için medyayı oynat/duraklat + Sonraki parça + Bir uygulama için sonraki parça + %s için sonraki parça + Önceki parça + Bir uygulama için önceki parça + %s için önceki parça + Hızlı ileri sar + Bir uygulama için hızlı ileri sar + %s için hızlı ileri sar + Tüm medya uygulamaları hızlı ileri sarmayı desteklemez. Örn. Google Play Music. + Geri sar + Bir uygulama için geri sar + %s için geri sar + Tüm medya uygulamaları geri sarmayı desteklemez. Örn. Google Play Music. + Geri dön + Ana ekrana git + Son uygulamaları aç + Menüyü aç + Bölünmüş ekranı aç/kapat + Son uygulamaya git (Son uygulamalara çift bas) + Flaş ışığını aç/kapat + Flaş ışığını aç + Flaş ışığını kapat + Flaş ışığını aç/kapat + Flaş ışığını aç/kapat (%s) + Flaş ışığını aç + Flaş ışığını aç (%s) + Flaş ışığını kapat + Flaş ışığı parlaklığını değiştir + Flaş ışığını %s artır + Flaş ışığını %s azalt + Ön flaş ışığını aç/kapat + Ön flaş ışığını aç/kapat (%s) + Ön flaş ışığını aç + Ön flaş ışığını aç (%s) + Ön flaş ışığını kapat + Ön flaş ışığı parlaklığını değiştir + Ön flaş ışığını %s artır + Ön flaş ışığını %s azalt + NFC\'yi aç + NFC\'yi kapat + NFC\'yi aç/kapat + Ekran görüntüsü al + Sesli asistanı başlat + Cihaz asistanını başlat + Kamerayı aç + Cihazı kilitle + Cihazı güvenli bir şekilde kilitle + Tekrar giriş yapmak için yalnızca PIN\'inizi kullanabilirsiniz. Parmak izi tarayıcı ve yüz tanıma devre dışı bırakılacaktır. Bu, Android Pie 9.0 öncesi root olmayan cihazları kilitlemenin bulduğum tek güvenilir yoludur. + Cihazı uyut/uyandır + Ekran kapalıyken tetikleyiciyi algılama seçeneğini açmanız gerekiyor! + Hiçbir şey yapma + İmleci sona taşı + Bu eylem bazı uygulamalarda beklendiği gibi çalışmayabilir. + Klavyeyi aç/kapat + Bu eylem yalnızca klavyenin gösterilmesi gereken bir giriş alanına dokunduğunuzda çalışır. + Klavyeyi göster + Klavyeyi gizle + Klavye seçiciyi göster + Klavyeyi değiştir + %s klavyesine geç + Kes + Kopyala + Yapıştır + İmlecin olduğu kelimeyi seç + Ayarları aç + Güç menüsünü göster + Uçak modunu aç/kapat + Uçak modunu aç + Uçak modunu devre dışı bırak + Uygulamayı başlat + Bazı cihazlar, uygulamaların arka planda başka uygulamaları başlatabilmesi için izne ihtiyaç duyar. Web sitemizdeki talimatları görmek için \"Daha fazla oku\"ya dokunun. + Daha fazla oku + Yoksay + Uyarı! + Uygulama kısayolunu başlat + Tuş kodunu gir + Tuş olayını gir + Ekrana dokun + Ekranı kaydır + Ekranı sıkıştır + Metin gir + URL aç + Intent gönder + Telefon araması başlat + Telefon aramasını cevapla + Telefon aramasını sonlandır + Ses çal + En son bildirimi kaldır + Tüm bildirimleri kaldır + Cihaz kontrol ekranı + HTTP isteği + HTTP Yöntemi + Açıklama + Boş bırakılamaz! + URL + Boş bırakılamaz! + Hatalı URL. http:// kısmını unuttunuz mu? + İstek gövdesi (opsiyonel) + Yetkilendirme başlığı (opsiyonel) + Gerekirse \'Bearer\' ön ekini kullanın + Uygulama öğesiyle etkileşime geç + Key Mapper, menüler, sekmeler, düğmeler ve onay kutuları gibi uygulama öğelerini algılayabilir ve bunlarla etkileşime girebilir. Key Mapper\'ın ne yapmak istediğinizi bilmesi için, uygulama öğesiyle etkileşiminizi kaydetmeniz gerekmektedir. + Kaydetmeye başla + Kaydetmeyi durdur (%s dakika kaldı) + + %d öğe seçildi + %d öğe seçildi + + Tekrar kaydet + Uygulama öğesini seçin + Tuş haritanızın etkileşime geçmesini istediğiniz öğeyi seçin. + Aradığınızı bulamıyor musunuz? + Tüm uygulamalar uyumlu değildir. Uyumlu olmayan uygulamalar için bunun yerine Ekrana Dokun eylemini deneyebilirsiniz. + Olası etkileşimler + UI öğesiyle nasıl etkileşim kurmak istediğinizi seçin. + Etkileşim türünü filtrele + Herhangi biri + Dokun + Dokun ve basılı tut + Odakla + Seç + İleri kaydır + Geriye kaydır + Genişlet + Daralt + Bilinmeyen: %d + Etkileşim detayları + Açıklama + Uygulama + Metin / içerik açıklaması + Sınıf adı + Kaynak kimliğini görüntüle + Özgün kimlik + Etkileşim türü + Navigasyon + Ses + Medya + Klavye + Uygulamalar + Giriş + Kamera ve Ses + Bağlantı + İçerik + Arayüz + Telefon + Ekran + Bildirimler + Boolean + Boolean dizisi + Integer + Tamsayı dizisi + Dize + Dize dizisi + Uzun + Uzun dizi + Bayt + Bayt dizisi + Çift + Çift dizi + Karakter + Karakter dizisi + Kayan Nokta + Kayan Nokta dizisi + Kısa + Kısa dizi + Yalnızca \"true\" veya \"false\" olabilir + \"true\" ve \"false\" içeren virgülle ayrılmış bir liste. Örn. true,false,true + Java programlama dilinde geçerli bir Tamsayı. + Java programlama dilinde geçerli Tamsayıların virgülle ayrılmış bir listesi. Örn. 100,399 + Virgülle ayrılmış bir liste. Örn. kategori1,kategori2 + Herhangi bir metin. + Dizelerin virgülle ayrılmış bir listesi. Örn. dize1,dize2 + Java programlama dilinde geçerli bir Uzun. + Java programlama dilinde geçerli Uzunların virgülle ayrılmış bir listesi. Örn. 102302234234234,399083423234429 + Java programlama dilinde geçerli bir Bayt. + Java programlama dilinde geçerli Baytların virgülle ayrılmış bir listesi. Örn. 123,3 + Java programlama dilinde geçerli bir Çift. + Java programlama dilinde geçerli Çiftlerin virgülle ayrılmış bir listesi. Örn. 1.0,3.234 + Java programlama dilinde geçerli bir Karakter. Örn. \'a\' veya \'b\' + Java programlama dilinde geçerli Karakterlerin virgülle ayrılmış bir listesi. Örn. a,b,c + Java programlama dilinde geçerli bir Kayan Nokta. Örn. 3.145 + Java programlama dilinde geçerli Kayan Noktaların virgülle ayrılmış bir listesi. Örn. 1241.123 + Java programlama dilinde geçerli bir Kısa. Örn. 2342 + Java programlama dilinde geçerli Kısa sayıların virgülle ayrılmış bir listesi. Örn. 3242,12354 + Intent bayrakları bit bayrakları olarak saklanır. Bu bayraklar Intent\'in nasıl işleneceğini değiştirir. Bir Etkinlik Intent\'i için bu alan boş bırakılırsa, Key Mapper varsayılan olarak FLAG_ACTIVITY_NEW_TASK kullanır. Daha fazla bilgi için Android geliştirici belgelerini görmek üzere \'dokümanlar\'a dokunun. + Hızlı Başlangıç Kılavuzu + Eğer takılırsanız Hızlı Başlangıç Kılavuzu\'na göz atın. + GitHub + Web Sitesi + Çeviriler + Sürüm %s + Oyla + Değişiklik Günlüğü + Discord + Sıkıcı şeyler + Lisans + Bu uygulamanın açık kaynak lisansı. + Gizlilik Politikası + Kişisel bilgi toplamıyoruz ama işte bunu belirten bir gizlilik politikası. + Ekibimiz + Geliştirici + Kullanıcı Deneyimi Tasarımcısı + Çevirmen (Lehçe) + Çevirmen (Çekçe) + Çevirmen (İspanyolca) + Key Mapper: Yan Tuş + Herhangi bir asistan + Yan tuş/güç düğmesi + Sesli asistan + Gelişmiş tetikleyiciler + Geliştirici, reklamların sürdürülebilir veya kullanıcı dostu bir gelir modeli olduğuna inanmıyor, bu nedenle bu ücretli tetikleyiciler geliştirmeyi desteklemeye yardımcı oluyor ❤️. Ayrıca öncelikli destek de alacaksınız. + Yan tuş ve Asistan tetikleyicisi + Yan tuşunuzu, güç düğmenizi veya cihaz asistanınızı yeniden eşleyebileceğinizi biliyor muydunuz? Asistanı veya güç menüsünü başlatmak yerine, cihazınız seçtiğiniz bir eylemi gerçekleştirebilir. Ekran kapalıyken bile çalışır! + Yan tuş tetikleyici özelliğini satın almanız gerekiyor. + Daha fazla bilgi + Kayan düğmeleri satın almanız gerekiyor. + Düğme silindi. + Kayan düğme + Yan tuş tetikleyicisini ayarla + Dikkat! + Bu tetikleyiciyi nasıl ayarlayacağınızı açıklayan web sitemizdeki talimatları okumanız gerekiyor. Key Mapper size rehberlik etmeyecek. + Talimatları oku + Tetikleyici türünü seç + Satın alma doğrulanamıyor. İnternet bağlantınız var mı? + Kilidi aç (%s) + Kullan + Yükleniyor… + Fiyatı tekrar almayı dene + Satın alma iptal edildi. + Bu, yalnızca Google Play\'den Key Mapper indirilerek satın alınabilen ücretli bir özellik gerektiriyor. + Ağ hatası oluştu. İnternet bağlantınız var mı? + Bu ürün bulunamadı. + Google Play bir hata ile karşılaştı. + Google Play\'den satın alımın başarılı olduğuna dair onay bekliyoruz. Kartınız başarıyla ücretlendirildiğinde Key Mapper uygulamasını yeniden açın. + Geçersiz satın alma. + Ödeme bekleniyor + Bir şeyler ters gitti 😕 + Tekrar dene + Geliştiriciyle iletişime geç + Yan tuş tetikleyici özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın. + Kayan düğmeler özelliğini satın almanız gerekiyor! Tuş eşlemesine dokunun ve ardından \'Gelişmiş tetikleyiciler\'e tıklayarak satın alın. + Uygulamayı desteklediğiniz için teşekkürler ❤️! + Satın alma işleminiz başarılı oldu. Key Mapper\'ın ücretli bir kullanıcısı olarak uygulamayı kullanmanıza yardımcı olmak için öncelikli destek alacaksınız. Artık bu sayfada geliştiriciyle iletişime geçmek için bir düğme var. + Gelişmiş tetikleyiciler ücretli bir özelliktir ancak siz FOSS yapısını indirdiniz ve bu yapı Google Play faturalandırmasını içermiyor. Bu özelliğe erişmek için lütfen Key Mapper\'ı Google Play\'den indirin. + Play sürümünü indir + DPAD düğmelerini yeniden eşlemek ister misiniz? + Aşağıdaki adımları izleyerek Key Mapper GUI Klavyesini ayarlamanız gerekiyor. + 1. Klavye uygulamasını yükle + Yükle + Yüklendi + 2. Klavyeyi etkinleştir + Etkinleştir + Etkinleştirildi + 3. Klavyeyi kullan + Klavyeyi değiştir + Klavye seçildi + Kurulum tamamlandı! \'Bitti\'ye dokunun ve DPAD tetikleyiciniz çalışmalı. + Düğme algılanmadı mı? + Erişilebilirlik servisi yerine tetikleyicinizi kaydetmek için Key Mapper GUI Klavye uygulamasını deneyebilirsiniz. + Kurulum tamamlandı! \'Bitti\'ye dokunun ve tetikleyicinizi tekrar kaydetmeyi deneyin. Eğer çalışmazsa Android bunu yeniden eşlemeye izin vermiyor demektir 🫤. + Dokun + bu menüyü göster/gizle. + Düğmeleri herhangi bir uygulamada, hatta kilit ekranında bile yerleştirebilirsiniz. + Kapat + Yardım + Düzeni değiştir + Düğme ekle + Geri dön + Düğmeleri gizle + Düğmeleri göster + Sil + Yapılandır + Tetikleyici olarak kullan + Sil + Düğme metni (İpucu: emoji kullanın) + Düğme boyutu: + Kenar opaklığı: + Arka plan opaklığı: + İptal + Bitti + Düğmenin metni olmalı! + Kayan düğmeler + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Kayan düğme %s (%s) + Silinen kayan düğme + Kısıtlamalar + Bu düğmenin yalnızca bazı uygulamalarda ekranda olmasını ister misiniz? Bu tuş eşlemesi için “Kısıtlamalar” sekmesinde bir “Ön plandaki uygulama” kısıtlaması ekleyin. + Bir düzen seçin + Geri + Yardım + Yardım mı lazım? + Düğme oluştur + Düğme seç + Çık + Tetikleyici olarak kullanmadan önce bir kayan düğme oluşturmanız gerekiyor. + Tetikleyici olarak kullanmak için bir kayan düğme seçmeniz gerekiyor. + Düğmeyi yapılandır + Düzeni düzenle + Android 11 veya daha yeni bir sürüm gerektiriyor. + Yeterince düğmeniz yok mu? Artık kendi düğmelerinizi yapabilirsiniz! + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Tetikleyici + Eylemler + Kısıtlamalar + Seçenekler + Tuş eşlemeleri + Kayan düğmeler + Yeni tuş eşlemesi + Yeni düzen + Bir tuş eşlemesini yapılandırmak için dokunun.\nDaha fazla seçenek için uzun basın. + Bir tuş eşlemesi oluşturun! + Yeterince düğmeniz yok mu? + Kendi düğmelerinizi yapın! + Kayan düğmeler istediğiniz uygulamaların üzerinde görünür. Gerçek düğmeler gibi çalışırlar ve onları istediğiniz gibi yerleştirebilir, stil verebilir ve eşleyebilirsiniz. + Bu sayfayı gizle + Key Mapper\'ı desteklediğiniz için teşekkürler ❤️! + İlk düzeninizi oluşturun! + Düzenler, kayan düğmeleri gruplar halinde organize etmenizi sağlar. Hangi düğmelerin gösterilebileceğini etkilemez. Herhangi bir düzendeki herhangi bir düğme aynı anda gösterilebilir. + %s düzeninin adını değiştir + %s düzenini sil + Kayan düğmeler + Düğme yok + Düğmeleri düzenle + Düğme ekle + Düzen adını değiştir + Kaydet + Ad boş olamaz! + Ad benzersiz olmalı! + %s sil + Bu kayan düğme düzenini silmek istediğinizden emin misiniz? + Evet, sil + İptal + Kayan düzenleri gizle + Kayan düğmeleri, bir tetikleyici oluştururken Gelişmiş Tetikleyiciler düğmesinde bulabilirsiniz. + Kayan düğmeler + Menü + Sırala + Daha fazla + Yardım + Tümünü seç + Tüm seçimi kaldır + Seçimi durdur + Bir grup yukarı çık + Duraklatıldı + + 1 uyarı + %d uyarı + + Çalışıyor + Ayarlar + Grubu sil + Hakkında + Tümünü dışa aktar + İçe aktar + Klavye seç + İçe aktarılıyor… + İçe aktarma başarılı! + Dışa aktarılıyor… + Başarısız: %s + Dosya yükleniyor… + İçe aktarma başarılı! + İçe aktarılıyor… + Hata + Uygulamayı aç + + 1 tuş eşlemesi içe aktarılıyor + %d tuş eşlemesi içe aktarılıyor + + Mevcut tuş eşlemelerinizin tümünü değiştirmek mi yoksa listeye eklemek mi istiyorsunuz? + İptal + Kapat + Ekle + Değiştir + Çoğalt + Sil + Dışa aktar + Etkin + Devre dışı + Karışık + Gruba taşı + + 1 tuş eşlemesini sil + %d tuş eşlemesini sil + + Bu tuş eşlemelerini silmek istediğinizden emin misiniz? + Evet, sil + İptal + Dosyalara kaydet + Yeni grup + Yeni alt grup + Tümünü görüntüle + Gizle + Grup kısıtlamaları + Yeni kısıtlama + Grup kısıtlamasını sil + Bu grup + Kaldır + Düzenle + Tetikleyici tuş seçenekleri + Cihaz + Asistan türü + Parmak izi hareket türü + Tıklama türü + Kayan düğme kullan + Parmak izi hareketi kullan + Yan tuş tetikleyicisi kullan + Kayan düğme + Yan tuş tetikleyicisi + Parmak izi hareketi + Kayan düğme: %s + Parmak izi okuyucuda yukarı kaydır + Parmak izi okuyucuda aşağı kaydır + Parmak izi okuyucuda sola kaydır + Parmak izi okuyucuda sağa kaydır + Gelişmiş tetikleyiciler + Kaldır + Düzenle + Test et + Tetiklendiğinde tuş eşlemesinin ne yapacağını ayarlamak için eylemler ekleyin. + Son kullanılan eylemler + Eylem seçenekleri + Sıfırla + Varsayılan: %s + Bir eylem seçin + Ara… + Burada hiçbir şey yok! + Eylemi sil + Bu eylemi silmek istediğinizden emin misiniz? + Evet, sil + İptal + Tuş eşlemesi tetiklendiğinde bu eylemleri çalıştır: + Taraf seç + Parlaklık + Minimum + Yarı + Maksimum + Test et + Android 13 veya daha yeni bir sürüm gerektiriyor. + Bu cihaz parlaklık değişikliğine izin vermiyor. + Parlaklık değişikliği + Desteklenmiyor + Tuş eşlemelerinin yalnızca belirli durumlarda çalışmasını istiyorsanız kısıtlamalar ekleyin. + Son kullanılan kısıtlamalar + Kaldır + Kısıtlamayı sil + Bu kısıtlamayı silmek istediğinizden emin misiniz? + Evet, sil + İptal + Mantık modu + Bir kısıtlama seçin + Bu tuş eşlemesi yalnızca şu durumlarda çalışır: + Adsız grup + Grup adını düzenle + Grup adını kaydet + Ad benzersiz olmalı! + Ana Sayfa + Grubu sil + %s grubunu sil + Bu grubu silmek istediğinizden emin misiniz? Bu gruptaki ve alt gruplarındaki tüm tuş eşlemeleri de silinecek! + Evet, sil + İptal + + +%d devralınan kısıtlamalar + +%d devralınan kısıtlamalar + diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/ar/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/ar/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/ar/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/full_description.txt b/fastlane/metadata/android/cs_CZ/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/cs_CZ/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/short_description.txt b/fastlane/metadata/android/cs_CZ/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/cs_CZ/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/title.txt b/fastlane/metadata/android/cs_CZ/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/cs_CZ/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/full_description.txt b/fastlane/metadata/android/de_DE/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/de_DE/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/short_description.txt b/fastlane/metadata/android/de_DE/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/de_DE/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/title.txt b/fastlane/metadata/android/de_DE/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/de_DE/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/full_description.txt b/fastlane/metadata/android/es_ES/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/es_ES/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/short_description.txt b/fastlane/metadata/android/es_ES/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/es_ES/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/title.txt b/fastlane/metadata/android/es_ES/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/es_ES/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/full_description.txt b/fastlane/metadata/android/fr_FR/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/fr_FR/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/short_description.txt b/fastlane/metadata/android/fr_FR/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/fr_FR/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/title.txt b/fastlane/metadata/android/fr_FR/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/fr_FR/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/full_description.txt b/fastlane/metadata/android/hu_HU/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/hu_HU/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/short_description.txt b/fastlane/metadata/android/hu_HU/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/hu_HU/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/title.txt b/fastlane/metadata/android/hu_HU/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/hu_HU/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/full_description.txt b/fastlane/metadata/android/id_ID/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/id_ID/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/short_description.txt b/fastlane/metadata/android/id_ID/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/id_ID/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/title.txt b/fastlane/metadata/android/id_ID/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/id_ID/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/full_description.txt b/fastlane/metadata/android/ka_GE/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/ka_GE/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/short_description.txt b/fastlane/metadata/android/ka_GE/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/ka_GE/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/title.txt b/fastlane/metadata/android/ka_GE/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/ka_GE/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/full_description.txt b/fastlane/metadata/android/ko_KR/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/ko_KR/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/short_description.txt b/fastlane/metadata/android/ko_KR/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/ko_KR/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/title.txt b/fastlane/metadata/android/ko_KR/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/ko_KR/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/full_description.txt b/fastlane/metadata/android/pl_PL/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/pl_PL/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/short_description.txt b/fastlane/metadata/android/pl_PL/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/pl_PL/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/title.txt b/fastlane/metadata/android/pl_PL/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/pl_PL/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/full_description.txt b/fastlane/metadata/android/pt_BR/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/pt_BR/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/short_description.txt b/fastlane/metadata/android/pt_BR/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/pt_BR/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/title.txt b/fastlane/metadata/android/pt_BR/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/pt_BR/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/full_description.txt b/fastlane/metadata/android/ru_RU/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/ru_RU/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/short_description.txt b/fastlane/metadata/android/ru_RU/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/ru_RU/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/title.txt b/fastlane/metadata/android/ru_RU/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/ru_RU/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/sk/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/sk/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/sk/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/full_description.txt b/fastlane/metadata/android/tr_TR/full_description.txt new file mode 100644 index 0000000000..b5be52ef5e --- /dev/null +++ b/fastlane/metadata/android/tr_TR/full_description.txt @@ -0,0 +1,55 @@ +# Klavyenizde veya oyun kumandanızda özel makrolar oluşturun, herhangi bir uygulamada ekran üstü düğmeler yapın ve ses düğmelerinizden yeni işlevler açın! + +Key Mapper, çok çeşitli düğme ve tuşları destekler*: + +- TÜM telefon düğmeleriniz (ses VE yan tuş) +- Oyun kumandaları (D-pad, ABXY ve çoğu diğer tuşlar) +- Klavyeler +- Kulaklık setleri ve kulaklıklar +- Parmak izi sensörü + +Yeterli tuş yok mu? Kendi ekran üstü düğme düzenlerinizi tasarlayın ve bunları gerçek tuşlar gibi yeniden atayın! + + +## Ne tür kısayollar oluşturabilirim? +-------------------------- + +100'den fazla bireysel eylemle, sınır gökyüzüdür. +Ekran dokunuşları ve hareketleri, klavye girişleri, uygulama açma, medya kontrolü ve hatta diğer uygulamalara doğrudan intent gönderme ile karmaşık makrolar oluşturun. + + +## Ne kadar kontrole sahibim? +--------------------------- + +TETİKLEYİCİLER: Bir tuş haritasını nasıl tetikleyeceğinize siz karar verirsiniz. Uzun basma, çift basma, istediğiniz kadar basma! Farklı cihazlardaki tuşları birleştirin ve hatta ekran üstü düğmelerinizi de dahil edin. + +EYLEMLER: Yapmak istediğiniz şey için özel makrolar tasarlayın. 100'den fazla eylemi birleştirin ve her biri arasındaki gecikmeyi seçin. Yavaş görevleri otomatikleştirmek ve hızlandırmak için tekrarlayan eylemler ayarlayın. + +KISITLAMALAR: Tuş haritalarının ne zaman çalışacağını ve ne zaman çalışmayacağını siz seçersiniz. Sadece belirli bir uygulamada mı gerekli? Ya da medya oynatılırken mi? Kilit ekranınızda mı? Maksimum kontrol için tuş haritalarınızı kısıtlayın. + +* Çoğu cihaz zaten desteklenmektedir ve zamanla yeni cihazlar eklenmektedir. Sizin için çalışmıyorsa bize bildirin, cihazınıza öncelik verebiliriz. + +Şu anda desteklenmeyen: + - Fare düğmeleri + - Oyun kumandalarındaki joystick ve tetikler (LT, RT) + + +Güvenlik ve erişilebilirlik hizmetleri +--------------------------- + +Bu uygulama, odaktaki uygulamayı algılamak ve tuş basımlarını kullanıcı tarafından tanımlanan tuş haritalarına uyarlamak için Android Erişilebilirlik API’sini kullanan Key Mapper Erişilebilirlik hizmetimizi içermektedir. Ayrıca, diğer uygulamaların üzerinde yardımcı Floating Button (Yüzen Düğme) katmanları çizmek için de kullanılmaktadır. + +Erişilebilirlik hizmetini çalıştırmayı kabul ettiğinizde, uygulama cihazınızı kullanırken tuş vuruşlarını izleyebilecektir. Ayrıca, uygulamada bu hareketleri kullanıyorsanız, kaydırma ve yakınlaştırma/daraltma hareketlerini de taklit edecektir. + +Herhangi bir kullanıcı verisi toplamayacak veya herhangi bir veriyi göndermek üzere internete bağlanmayacaktır. + +Erişilebilirlik hizmetimiz, yalnızca kullanıcı cihazındaki fiziksel bir tuşa bastığında tetiklenir. Kullanıcı, sistem erişilebilirlik ayarlarından bu hizmeti istediği zaman kapatabilir. + +Discord topluluğumuza gelip merhaba deyin! +www.keymapper.club + +Kodu kendiniz görün! (Açık kaynak) +code.keymapper.club + +Belgeleri okuyun: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/short_description.txt b/fastlane/metadata/android/tr_TR/short_description.txt new file mode 100644 index 0000000000..9dbca48bd4 --- /dev/null +++ b/fastlane/metadata/android/tr_TR/short_description.txt @@ -0,0 +1 @@ +HER ŞEY için kısayollar oluşturun! Ses, güç, klavye veya kayan düğmeleri yeniden atayın! \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/title.txt b/fastlane/metadata/android/tr_TR/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/tr_TR/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/uk/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/uk/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/uk/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/vi/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/full_description.txt b/fastlane/metadata/android/zh_CN/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/zh_CN/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/short_description.txt b/fastlane/metadata/android/zh_CN/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/zh_CN/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/title.txt b/fastlane/metadata/android/zh_CN/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/zh_CN/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/full_description.txt b/fastlane/metadata/android/zh_TW/full_description.txt new file mode 100644 index 0000000000..6863b48341 --- /dev/null +++ b/fastlane/metadata/android/zh_TW/full_description.txt @@ -0,0 +1,55 @@ +Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! + +Key Mapper supports a huge variety of buttons and keys*: + +- ALL your phone buttons (volume AND side key) +- Game controllers (D-pad, ABXY, and most others) +- Keyboards +- Headsets and headphones +- Fingerprint sensor + +Not enough keys? Design your own on-screen button layouts and remap those just like real keys! + + +What shortcuts can I make? +-------------------------- + +With over 100 individual actions, the sky is the limit. +Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. + + +How much control do I have? +--------------------------- + +TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. + +ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. + +CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. + +* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. + +Not currently supported: + - Mouse buttons + - Joysticks and triggers (LT,RT) on gamepads + + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + +Come say hi in our Discord community! +www.keymapper.club + +See the code for yourself! (Open source) +code.keymapper.club + +Read the documentation: +docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/short_description.txt b/fastlane/metadata/android/zh_TW/short_description.txt new file mode 100644 index 0000000000..ecaa2a662d --- /dev/null +++ b/fastlane/metadata/android/zh_TW/short_description.txt @@ -0,0 +1 @@ +Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/title.txt b/fastlane/metadata/android/zh_TW/title.txt new file mode 100644 index 0000000000..9810cafe1e --- /dev/null +++ b/fastlane/metadata/android/zh_TW/title.txt @@ -0,0 +1 @@ +Key Mapper & Floating Buttons \ No newline at end of file From 5e18b3e25b60f3537110323bb2121fc957ff923f Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 14:48:51 +0200 Subject: [PATCH 49/69] #257 add option to show additional elements that dumps all the elements from the window --- .../20.json | 460 ++++++++++++++++++ .../uielement/ChooseUiElementScreen.kt | 24 +- .../uielement/InteractUiElementScreen.kt | 1 + .../uielement/InteractUiElementViewModel.kt | 26 +- .../actions/uielement/NodeInteractionType.kt | 1 - .../sds100/keymapper/backup/BackupManager.kt | 3 + .../sds100/keymapper/data/db/AppDatabase.kt | 5 +- .../data/db/dao/AccessibilityNodeDao.kt | 3 + .../data/entities/AccessibilityNodeEntity.kt | 15 + .../data/migration/AutoMigration19To20.kt | 5 + .../AccessibilityNodeRecorder.kt | 69 ++- .../BaseAccessibilityServiceController.kt | 29 +- .../accessibility/IAccessibilityService.kt | 1 + .../accessibility/MyAccessibilityService.kt | 10 + app/src/main/res/values/strings.xml | 4 +- 15 files changed, 615 insertions(+), 41 deletions(-) create mode 100644 app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json create mode 100644 app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt diff --git a/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json new file mode 100644 index 0000000000..3ee2ff236a --- /dev/null +++ b/app/schemas/io.github.sds100.keymapper.data.db.AppDatabase/20.json @@ -0,0 +1,460 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "f2f5eac59b7bdee472c0dd7ff9bae4b2", + "entities": [ + { + "tableName": "keymaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trigger` TEXT NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, `uid` TEXT NOT NULL, `group_uid` TEXT, FOREIGN KEY(`group_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupUid", + "columnName": "group_uid", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_keymaps_uid", + "unique": true, + "columnNames": [ + "uid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_keymaps_uid` ON `${TABLE_NAME}` (`uid`)" + } + ], + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "fingerprintmaps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `action_list` TEXT NOT NULL, `constraint_list` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `extras` TEXT NOT NULL, `flags` INTEGER NOT NULL, `is_enabled` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actionList", + "columnName": "action_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraint_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `severity` INTEGER NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "severity", + "columnName": "severity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "floating_layouts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_layouts_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_floating_layouts_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "floating_buttons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `layout_uid` TEXT NOT NULL, `text` TEXT NOT NULL, `button_size` INTEGER NOT NULL, `x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `orientation` TEXT NOT NULL, `display_width` INTEGER NOT NULL, `display_height` INTEGER NOT NULL, `border_opacity` REAL, `background_opacity` REAL, PRIMARY KEY(`uid`), FOREIGN KEY(`layout_uid`) REFERENCES `floating_layouts`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutUid", + "columnName": "layout_uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "buttonSize", + "columnName": "button_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orientation", + "columnName": "orientation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayWidth", + "columnName": "display_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayHeight", + "columnName": "display_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "borderOpacity", + "columnName": "border_opacity", + "affinity": "REAL" + }, + { + "fieldPath": "backgroundOpacity", + "columnName": "background_opacity", + "affinity": "REAL" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_floating_buttons_layout_uid", + "unique": false, + "columnNames": [ + "layout_uid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_floating_buttons_layout_uid` ON `${TABLE_NAME}` (`layout_uid`)" + } + ], + "foreignKeys": [ + { + "table": "floating_layouts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "layout_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `name` TEXT NOT NULL, `constraints` TEXT NOT NULL, `constraint_mode` INTEGER NOT NULL, `parent_uid` TEXT, `last_opened_date` INTEGER, PRIMARY KEY(`uid`), FOREIGN KEY(`parent_uid`) REFERENCES `groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintList", + "columnName": "constraints", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "constraintMode", + "columnName": "constraint_mode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentUid", + "columnName": "parent_uid", + "affinity": "TEXT" + }, + { + "fieldPath": "lastOpenedDate", + "columnName": "last_opened_date", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "foreignKeys": [ + { + "table": "groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parent_uid" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "accessibility_nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `package_name` TEXT NOT NULL, `text` TEXT, `content_description` TEXT, `class_name` TEXT, `view_resource_id` TEXT, `unique_id` TEXT, `actions` INTEGER NOT NULL, `interacted` INTEGER NOT NULL DEFAULT false, `tooltip` TEXT DEFAULT NULL, `hint` TEXT DEFAULT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT" + }, + { + "fieldPath": "contentDescription", + "columnName": "content_description", + "affinity": "TEXT" + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT" + }, + { + "fieldPath": "viewResourceId", + "columnName": "view_resource_id", + "affinity": "TEXT" + }, + { + "fieldPath": "uniqueId", + "columnName": "unique_id", + "affinity": "TEXT" + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interacted", + "columnName": "interacted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "tooltip", + "columnName": "tooltip", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "hint", + "columnName": "hint", + "affinity": "TEXT", + "defaultValue": "NULL" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2f5eac59b7bdee472c0dd7ff9bae4b2')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index 612cb913af..e38a7bf080 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State +import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions @@ -59,6 +60,7 @@ fun ChooseElementScreen( onQueryChange: (String) -> Unit = {}, onClickElement: (Long) -> Unit = {}, onSelectInteractionType: (NodeInteractionType?) -> Unit = {}, + onAdditionalElementsCheckedChange: (Boolean) -> Unit = {}, ) { var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } @@ -144,10 +146,25 @@ fun ChooseElementScreen( is State.Data -> { val listItems = state.data.listItems + CheckBoxText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.action_interact_ui_element_checkbox_additional_elements), + isChecked = state.data.showAdditionalElements, + onCheckedChange = onAdditionalElementsCheckedChange, + ) + + Spacer(modifier = Modifier.height(8.dp)) + if (listItems.isEmpty()) { - EmptyList(modifier = Modifier.fillMaxSize()) + EmptyList( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) } else { - KeyMapperDropdownMenu( + KeyMapperDropdownMenu( modifier = Modifier.padding(horizontal = 16.dp), expanded = interactionTypeExpanded, onExpandedChange = { interactionTypeExpanded = it }, @@ -309,6 +326,7 @@ private fun Empty() { listItems = emptyList(), interactionTypes = emptyList(), selectedInteractionType = null, + showAdditionalElements = false, ), ), query = "Key Mapper", @@ -343,6 +361,7 @@ private fun Loaded() { NodeInteractionType.LONG_CLICK, NodeInteractionType.SCROLL_FORWARD, ), + interacted = true, ), ) @@ -354,6 +373,7 @@ private fun Loaded() { NodeInteractionType.LONG_CLICK to "Tap and hold", ), selectedInteractionType = null, + showAdditionalElements = true, ) KeyMapperTheme { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 482b679396..0c0ec02e9e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -161,6 +161,7 @@ fun InteractUiElementScreen( navController.popBackStack(route = DEST_LANDING, inclusive = false) }, onSelectInteractionType = viewModel::onSelectInteractionTypeFilter, + onAdditionalElementsCheckedChange = viewModel::onAdditionalElementsCheckedChanged, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 80d255273b..dd1a267519 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -139,13 +139,20 @@ class InteractUiElementViewModel( private val selectedInteractionTypeFilter = MutableStateFlow(null) + private val showAdditionalElements: MutableStateFlow = MutableStateFlow(false) + private val filteredElementListItems = combine( elementListItems, elementSearchQuery, selectedInteractionTypeFilter, - ) { state, query, interactionType -> + showAdditionalElements, + ) { state, query, interactionType, showAdditionalElements -> state.mapData { listItems -> listItems.filter { model -> + if (!showAdditionalElements && !model.interacted) { + return@filter false + } + if (interactionType != null && !model.interactionTypes.contains(interactionType)) { return@filter false } @@ -166,7 +173,8 @@ class InteractUiElementViewModel( filteredElementListItems, interactionTypesFilterItems, selectedInteractionTypeFilter, - ) { listItemsState, interactionTypesState, selectedInteractionType -> + showAdditionalElements, + ) { listItemsState, interactionTypesState, selectedInteractionType, showAdditionalElements -> val listItems = listItemsState.dataOrNull() ?: return@combine State.Loading val interactionTypes = interactionTypesState.dataOrNull() ?: return@combine State.Loading @@ -174,6 +182,7 @@ class InteractUiElementViewModel( listItems = listItems, interactionTypes = interactionTypes, selectedInteractionType = selectedInteractionType, + showAdditionalElements = showAdditionalElements, ) State.Data(newState) }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) @@ -288,6 +297,10 @@ class InteractUiElementViewModel( } } + fun onAdditionalElementsCheckedChanged(checked: Boolean) { + showAdditionalElements.update { checked } + } + private suspend fun startRecording() { useCase.startRecording().onFailure { error -> if (error == Error.AccessibilityServiceDisabled) { @@ -325,9 +338,10 @@ class InteractUiElementViewModel( nodeViewResourceId = resourceIdText, nodeText = node.text ?: node.contentDescription, nodeClassName = node.className, - nodeUniqueId = node.uniqueId?.toString(), + nodeUniqueId = node.uniqueId, interactionTypesText = node.actions.joinToString { getInteractionTypeString(it) }, interactionTypes = node.actions, + interacted = node.interacted, ) } @@ -352,7 +366,6 @@ class InteractUiElementViewModel( NodeInteractionType.CLICK -> getString(R.string.action_interact_ui_element_interaction_type_click) NodeInteractionType.LONG_CLICK -> getString(R.string.action_interact_ui_element_interaction_type_long_click) NodeInteractionType.FOCUS -> getString(R.string.action_interact_ui_element_interaction_type_focus) - NodeInteractionType.SELECT -> getString(R.string.action_interact_ui_element_interaction_type_select) NodeInteractionType.SCROLL_FORWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_forward) NodeInteractionType.SCROLL_BACKWARD -> getString(R.string.action_interact_ui_element_interaction_type_scroll_backward) NodeInteractionType.EXPAND -> getString(R.string.action_interact_ui_element_interaction_type_expand) @@ -399,6 +412,7 @@ data class SelectUiElementState( val listItems: List, val interactionTypes: List>, val selectedInteractionType: NodeInteractionType?, + val showAdditionalElements: Boolean, ) data class UiElementListItemModel( @@ -409,4 +423,8 @@ data class UiElementListItemModel( val nodeUniqueId: String?, val interactionTypesText: String, val interactionTypes: Set, + /** + * Whether the user interacted with this element. + */ + val interacted: Boolean, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt index f31a6520f5..8d02874b45 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/NodeInteractionType.kt @@ -6,7 +6,6 @@ enum class NodeInteractionType(val accessibilityActionId: Int) { CLICK(AccessibilityNodeInfo.ACTION_CLICK), LONG_CLICK(AccessibilityNodeInfo.ACTION_LONG_CLICK), FOCUS(AccessibilityNodeInfo.ACTION_FOCUS), - SELECT(AccessibilityNodeInfo.ACTION_SELECT), SCROLL_FORWARD(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD), SCROLL_BACKWARD(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD), EXPAND(AccessibilityNodeInfo.ACTION_EXPAND), diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index f2e22b2ce8..8e524a033b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -252,6 +252,9 @@ class BackupManagerImpl( // Do nothing. Just added the accessibility node table. JsonMigration(18, 19) { json -> json }, + + // Do nothing. Just added columns to the accessibility node table. + JsonMigration(19, 20) { json -> json }, ) if (keyMapListJsonArray != null) { diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt index e4644b1055..bba74e2b66 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/AppDatabase.kt @@ -32,6 +32,7 @@ import io.github.sds100.keymapper.data.migration.AutoMigration14To15 import io.github.sds100.keymapper.data.migration.AutoMigration15To16 import io.github.sds100.keymapper.data.migration.AutoMigration16To17 import io.github.sds100.keymapper.data.migration.AutoMigration18To19 +import io.github.sds100.keymapper.data.migration.AutoMigration19To20 import io.github.sds100.keymapper.data.migration.Migration10To11 import io.github.sds100.keymapper.data.migration.Migration11To12 import io.github.sds100.keymapper.data.migration.Migration13To14 @@ -60,6 +61,8 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class), // Adds accessibility node table AutoMigration(from = 18, to = 19, spec = AutoMigration18To19::class), + // Adds interacted, tooltip, and hint fields to accessibility node entity + AutoMigration(from = 19, to = 20, spec = AutoMigration19To20::class), ], ) @TypeConverters( @@ -72,7 +75,7 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 abstract class AppDatabase : RoomDatabase() { companion object { const val DATABASE_NAME = "key_map_database" - const val DATABASE_VERSION = 19 + const val DATABASE_VERSION = 20 val MIGRATION_1_2 = object : Migration(1, 2) { diff --git a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt index ed2bd36250..1aa13584a5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/db/dao/AccessibilityNodeDao.kt @@ -19,6 +19,9 @@ interface AccessibilityNodeDao { const val KEY_VIEW_RESOURCE_ID = "view_resource_id" const val KEY_UNIQUE_ID = "unique_id" const val KEY_ACTIONS = "actions" + const val KEY_INTERACTED = "interacted" + const val KEY_TOOLTIP = "tooltip" + const val KEY_HINT = "hint" } @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_ID = (:id)") diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt index ca3ba0ee1c..f84769395c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/AccessibilityNodeEntity.kt @@ -8,9 +8,12 @@ import io.github.sds100.keymapper.actions.uielement.NodeInteractionType import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ACTIONS import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CLASS_NAME import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_CONTENT_DESCRIPTION +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_HINT import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_ID +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_INTERACTED import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_PACKAGE_NAME import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_TEXT +import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_TOOLTIP import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_UNIQUE_ID import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.KEY_VIEW_RESOURCE_ID import io.github.sds100.keymapper.data.db.dao.AccessibilityNodeDao.Companion.TABLE_NAME @@ -45,4 +48,16 @@ data class AccessibilityNodeEntity( @ColumnInfo(name = KEY_ACTIONS) val actions: Set, + + /** + * Whether the user interacted with this node. + */ + @ColumnInfo(name = KEY_INTERACTED, defaultValue = false.toString()) + val interacted: Boolean, + + @ColumnInfo(name = KEY_TOOLTIP, defaultValue = "NULL") + val tooltip: String?, + + @ColumnInfo(name = KEY_HINT, defaultValue = "NULL") + val hint: String?, ) : Parcelable diff --git a/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt new file mode 100644 index 0000000000..edec936175 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/data/migration/AutoMigration19To20.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.data.migration + +import androidx.room.migration.AutoMigrationSpec + +class AutoMigration19To20 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt index b0e1250a56..becc2fcf78 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityNodeRecorder.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.system.accessibility -import android.graphics.Rect +import android.accessibilityservice.AccessibilityService import android.os.Build import android.os.CountDownTimer import android.view.accessibility.AccessibilityEvent @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.update class AccessibilityNodeRecorder( private val nodeRepository: AccessibilityNodeRepository, + private val service: AccessibilityService, ) { companion object { private const val RECORD_DURATION = 60000L @@ -60,51 +61,54 @@ class AccessibilityNodeRecorder( return } - val source = event.source ?: return - val sourceBounds = Rect() - source.getBoundsInScreen(sourceBounds) - - val root: AccessibilityNodeInfo = source.window.root ?: return - - // This searches for all nodes that are within the bounds of the source of the - // AccessibilityEvent because the source is not necessarily the element - // the user wants to tap. - val entities = getNodesInBounds(root, sourceBounds).toTypedArray() - nodeRepository.insert(*entities) + if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED || + event.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED + ) { + val source = event.source ?: return + + buildNodeEntity(source, interacted = true)?.also { nodeRepository.insert(it) } + } else if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + // Only dump the whole window when a window is added because there can be + // many windows changed events sent in rapid succession. + val windowRoot: AccessibilityNodeInfo = service.rootInActiveWindow ?: return + + // This searches for all nodes that are within the bounds of the source of the + // AccessibilityEvent because the source is not necessarily the element + // the user wants to tap. + val entities = getNodesRecursively(windowRoot).toTypedArray() + nodeRepository.insert(*entities) + } } - /** - * Get all the nodes that are within the given bounds. - */ - private fun getNodesInBounds( + private fun getNodesRecursively( node: AccessibilityNodeInfo, - bounds: Rect, ): Set { val set = mutableSetOf() - val nodeBounds = Rect() - node.getBoundsInScreen(nodeBounds) - - if (bounds.contains(nodeBounds)) { - val entity = buildNodeEntity(node) + val entity = buildNodeEntity(node, interacted = false) - if (entity != null) { - set.add(entity) - } + if (entity != null) { + set.add(entity) } if (node.childCount > 0) { for (i in 0 until node.childCount) { val child = node.getChild(i) ?: continue - set.addAll(getNodesInBounds(child, bounds)) + set.addAll(getNodesRecursively(child)) } } return set } - private fun buildNodeEntity(source: AccessibilityNodeInfo): AccessibilityNodeEntity? { + /** + * @param interacted Whether the user interacted with this node. + */ + private fun buildNodeEntity( + source: AccessibilityNodeInfo, + interacted: Boolean, + ): AccessibilityNodeEntity? { val interactionTypes = source.actionList.mapNotNull { action -> NodeInteractionType.entries.find { it.accessibilityActionId == action.id } }.distinct() @@ -125,6 +129,17 @@ class AccessibilityNodeRecorder( null }, actions = interactionTypes.toSet(), + interacted = interacted, + tooltip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + source.tooltipText?.toString() + } else { + null + }, + hint = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + source.hintText?.toString() + } else { + null + }, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index d0a3347b75..1f53574fe0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -85,6 +85,8 @@ abstract class BaseAccessibilityServiceController( * How long should the accessibility service record a trigger in seconds. */ private const val RECORD_TRIGGER_TIMER_LENGTH = 5 + + private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L } private val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -107,7 +109,7 @@ abstract class BaseAccessibilityServiceController( ) private val accessibilityNodeRecorder: AccessibilityNodeRecorder = - AccessibilityNodeRecorder(nodeRepository) + AccessibilityNodeRecorder(nodeRepository, service) private var recordingTriggerJob: Job? = null private val recordingTrigger: Boolean @@ -179,6 +181,9 @@ abstract class BaseAccessibilityServiceController( val serviceEventTypes: MutableStateFlow = MutableStateFlow(AccessibilityEvent.TYPE_WINDOWS_CHANGED) + private val serviceNotificationTimeout: MutableStateFlow = + MutableStateFlow(DEFAULT_NOTIFICATION_TIMEOUT) + init { serviceFlags.onEach { flags -> @@ -202,6 +207,13 @@ abstract class BaseAccessibilityServiceController( } }.launchIn(coroutineScope) + serviceNotificationTimeout.onEach { timeout -> + // check that it isn't null because this can only be called once the service is bound + if (service.notificationTimeout != null) { + service.notificationTimeout = timeout + } + }.launchIn(coroutineScope) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { combine( detectKeyMapsUseCase.requestFingerprintGestureDetection, @@ -274,11 +286,11 @@ abstract class BaseAccessibilityServiceController( val imeInputFocusEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED + // Listen to WINDOWS_CHANGED event in case no events are received when the user + // interacts directly with elements. val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED or - AccessibilityEvent.TYPE_VIEW_LONG_CLICKED or - AccessibilityEvent.TYPE_VIEW_SELECTED or - AccessibilityEvent.TYPE_VIEW_SCROLLED + AccessibilityEvent.TYPE_WINDOWS_CHANGED coroutineScope.launch { combine( @@ -304,6 +316,14 @@ abstract class BaseAccessibilityServiceController( newEventTypes } + + serviceNotificationTimeout.update { + if (recordState is RecordAccessibilityNodeState.CountingDown) { + 0L + } else { + DEFAULT_NOTIFICATION_TIMEOUT + } + } }.collect() } } @@ -312,6 +332,7 @@ abstract class BaseAccessibilityServiceController( service.serviceFlags = serviceFlags.value service.serviceFeedbackType = serviceFeedbackType.value service.serviceEventTypes = serviceEventTypes.value + service.notificationTimeout = serviceNotificationTimeout.value // check if fingerprint gestures are supported if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt index 59d9ac5e42..4e45865eb9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt @@ -40,6 +40,7 @@ interface IAccessibilityService { var serviceFlags: Int? var serviceFeedbackType: Int? var serviceEventTypes: Int? + var notificationTimeout: Long? fun performActionOnNode( findNode: (node: AccessibilityNodeModel) -> Boolean, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index c25ce900ec..81871a3575 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -124,6 +124,16 @@ class MyAccessibilityService : } } + override var notificationTimeout: Long? + get() = serviceInfo?.notificationTimeout + set(value) { + if (serviceInfo != null && value != null) { + serviceInfo = serviceInfo.apply { + notificationTimeout = value + } + } + } + private val relayServiceCallback: IKeyEventRelayServiceCallback = object : IKeyEventRelayServiceCallback.Stub() { override fun onKeyEvent(event: KeyEvent?): Boolean { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2dc876b22f..927a5b57f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,7 +25,7 @@ Restart accessibility service Share Nothing here! - Nothing here! + Key Mapper did not detect any interactions. Try showing additional elements. Stop repeating when… Trigger is released Trigger is pressed again @@ -1132,12 +1132,12 @@ Possible interactions Select how you want to interact with the UI element. Filter interaction type + Show additional elements Any Tap Tap and hold Focus - Select Scroll forward Scroll backward Expand From 110e13616e1c1d3ad8b33a6cb2a1f138c42775ec Mon Sep 17 00:00:00 2001 From: sds100 Date: Thu, 8 May 2025 15:16:35 +0200 Subject: [PATCH 50/69] #257 change how nodes are matched --- .../actions/PerformActionsUseCase.kt | 44 ++++++++++--------- .../BaseAccessibilityServiceController.kt | 7 +-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index fdfb55b8de..a3661b0721 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -844,7 +844,7 @@ class PerformActionsUseCaseImpl( matchAccessibilityNode(node, action) }, performAction = { AccessibilityNodeAction(action = action.nodeAction.accessibilityActionId) }, - ) + ).otherwise { Error.UiElementNotFound } } } } @@ -944,32 +944,36 @@ class PerformActionsUseCaseImpl( node: AccessibilityNodeModel, action: ActionData.InteractUiElement, ): Boolean { + if (!node.actions.contains(action.nodeAction.accessibilityActionId)) { + return false + } + if (compareIfNonNull(node.uniqueId, action.uniqueId)) { return true } - val viewResourceIdMatches = node.viewResourceId == action.viewResourceId - val classNameMatches = node.className == action.className + if (action.contentDescription == null && action.text == null) { + if (compareIfNonNull(node.viewResourceId, action.viewResourceId)) { + return true + } - if (compareIfNonNull( - node.contentDescription, - action.contentDescription, - ) && - viewResourceIdMatches && - classNameMatches - ) { - return true - } + if (compareIfNonNull(node.className, action.className)) { + return true + } + } else { + if (compareIfNonNull(node.contentDescription, action.contentDescription) || + compareIfNonNull(node.text, action.text) + ) { + if (action.viewResourceId != null) { + return node.viewResourceId == action.viewResourceId + } - if (compareIfNonNull(node.text, action.text) && - viewResourceIdMatches && - classNameMatches - ) { - return true - } + if (action.className != null) { + return node.className == action.className + } - if (viewResourceIdMatches) { - return true + return true + } } return false diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index 1f53574fe0..cd1a7ef200 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -286,11 +286,8 @@ abstract class BaseAccessibilityServiceController( val imeInputFocusEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED - // Listen to WINDOWS_CHANGED event in case no events are received when the user - // interacts directly with elements. - val recordNodeEvents = AccessibilityEvent.TYPE_VIEW_FOCUSED or - AccessibilityEvent.TYPE_VIEW_CLICKED or - AccessibilityEvent.TYPE_WINDOWS_CHANGED + val recordNodeEvents = + AccessibilityEvent.TYPE_VIEW_FOCUSED or AccessibilityEvent.TYPE_VIEW_CLICKED coroutineScope.launch { combine( From d56f7a7ac9878caa99d847894a70fe70366a353e Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 13:54:28 +0200 Subject: [PATCH 51/69] fix: export log files as .txt instead of .zip files. --- CHANGELOG.md | 1 + .../main/java/io/github/sds100/keymapper/logging/LogFragment.kt | 2 +- .../java/io/github/sds100/keymapper/system/files/FileUtils.kt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32babf419d..029b253347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ## Bug fixes - #1683 key event actions work in Minecraft and other apps again. +- Export log files as .txt instead of .zip files. ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) diff --git a/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt b/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt index 6e1effbd57..50760f51d0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/logging/LogFragment.kt @@ -44,7 +44,7 @@ class LogFragment : SimpleRecyclerViewFragment() { private val recyclerViewController by lazy { RecyclerViewController() } private val saveLogToFileLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { + registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_TEXT)) { it ?: return@registerForActivityResult viewModel.onPickFileToSaveTo(it.toString()) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt b/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt index d2a4b14628..d870339f02 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/files/FileUtils.kt @@ -26,6 +26,7 @@ object FileUtils { const val MIME_TYPE_AUDIO = "audio/*" const val MIME_TYPE_ZIP = "application/zip" const val MIME_TYPE_JSON = "text/json" + const val MIME_TYPE_TEXT = "text/plain" @SuppressLint("SimpleDateFormat") fun createFileDate(): String { From f79f738cb64ad9a24e3c78fa5c5c4c0507ecf1d7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 13:54:39 +0200 Subject: [PATCH 52/69] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 773194701f..1085b82907 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=112 +VERSION_CODE=114 VERSION_NUM=0 \ No newline at end of file From d900fe8019ef5463947916e58ba4ec185a7f0367 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 13:54:53 +0200 Subject: [PATCH 53/69] delete unused turkish translation --- app/src/main/res/values-tr/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e32a75e124..700903f43f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -885,7 +885,6 @@ Dokun Dokun ve basılı tut Odakla - Seç İleri kaydır Geriye kaydır Genişlet From 7d3744bc69afcdb06a569d06393d7f80adb05808 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 13:57:27 +0200 Subject: [PATCH 54/69] #257 show tooltip and hint text --- .../sds100/keymapper/actions/ActionData.kt | 2 ++ .../actions/ActionDataEntityMapper.kt | 12 ++++++++ .../uielement/ChooseUiElementScreen.kt | 8 ++++++ .../uielement/InteractUiElementScreen.kt | 14 +++++++++- .../uielement/InteractUiElementViewModel.kt | 28 +++++++++++++------ .../keymapper/data/entities/ActionEntity.kt | 2 ++ app/src/main/res/values/strings.xml | 3 +- 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 56dfd47265..9a2734bb14 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -883,6 +883,8 @@ sealed class ActionData : Comparable { val nodeAction: NodeInteractionType, val packageName: String, val text: String?, + val tooltip: String?, + val hint: String?, val contentDescription: String?, val className: String?, val viewResourceId: String?, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index 8eedce9b51..ce246ca4cf 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -557,6 +557,12 @@ object ActionDataEntityMapper { val text = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TEXT).valueOrNull() + val tooltip = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_TOOLTIP).valueOrNull() + + val hint = + entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_HINT).valueOrNull() + val className = entity.extras.getData(ActionEntity.EXTRA_ACCESSIBILITY_CLASS_NAME).valueOrNull() @@ -582,6 +588,8 @@ object ActionDataEntityMapper { packageName = packageName, text = text, contentDescription = contentDescription, + tooltip = tooltip, + hint = hint, className = className, viewResourceId = viewResourceId, uniqueId = uniqueId, @@ -855,6 +863,10 @@ object ActionDataEntityMapper { data.text?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TEXT, it)) } + data.tooltip?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_TOOLTIP, it)) } + + data.hint?.let { add(EntityExtra(ActionEntity.EXTRA_ACCESSIBILITY_HINT, it)) } + data.className?.let { add( EntityExtra( diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index e38a7bf080..4d262d60f2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -276,6 +276,13 @@ private fun UiElementListItem( ) } + if (model.nodeTooltipHint != null) { + TextWithLeadingLabel( + title = stringResource(R.string.action_interact_ui_element_tooltip_label), + text = model.nodeTooltipHint, + ) + } + if (model.nodeUniqueId != null) { TextWithLeadingLabel( title = stringResource(R.string.action_interact_ui_element_unique_id_label), @@ -355,6 +362,7 @@ private fun Loaded() { nodeClassName = "android.widget.ImageButton", nodeViewResourceId = "menu_button", nodeUniqueId = "123456789", + nodeTooltipHint = "Open menu", interactionTypesText = "Tap, Tap and hold, Scroll forward", interactionTypes = setOf( NodeInteractionType.CLICK, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 0c0ec02e9e..4b680630d9 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -530,6 +530,17 @@ private fun SelectedElementSection( Spacer(modifier = Modifier.height(8.dp)) + if (state.nodeToolTipHint != null) { + Text( + text = stringResource(R.string.action_interact_ui_element_tooltip_label), + style = MaterialTheme.typography.titleSmall, + ) + + Text(text = state.nodeToolTipHint, style = MaterialTheme.typography.bodyMedium) + } + + Spacer(modifier = Modifier.height(8.dp)) + if (state.nodeClassName != null) { Text( text = stringResource(R.string.action_interact_ui_element_class_name_label), @@ -575,7 +586,7 @@ private fun SelectedElementSection( Spacer(modifier = Modifier.height(8.dp)) - KeyMapperDropdownMenu( + KeyMapperDropdownMenu( expanded = interactionTypeExpanded, onExpandedChange = { interactionTypeExpanded = it }, values = state.interactionTypes, @@ -612,6 +623,7 @@ private fun PreviewSelectedElement() { appName = "Test App", appIcon = ComposeIconInfo.Drawable(appIcon), nodeText = "Test Node", + nodeToolTipHint = "Test tooltip", nodeClassName = "android.widget.ImageButton", nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", nodeUniqueId = "123", diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index dd1a267519..cc49251b80 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -89,6 +89,7 @@ class InteractUiElementViewModel( } }.stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) + private val selectedElementEntity = MutableStateFlow(null) private val _selectedElementState = MutableStateFlow(null) val selectedElementState: StateFlow = _selectedElementState.asStateFlow() @@ -160,6 +161,8 @@ class InteractUiElementViewModel( val modelString = buildString { append(model.nodeText) append(" ") + append(model.nodeTooltipHint) + append(" ") append(model.nodeClassName) append(" ") append(model.nodeViewResourceId) @@ -198,6 +201,7 @@ class InteractUiElementViewModel( appName = appName, appIcon = appIcon, nodeText = action.text ?: action.contentDescription, + nodeToolTipHint = action.tooltip ?: action.hint, nodeClassName = action.className, nodeViewResourceId = action.viewResourceId, nodeUniqueId = action.uniqueId, @@ -211,7 +215,9 @@ class InteractUiElementViewModel( fun onDoneClick() { val selectedElementState = _selectedElementState.value - if (selectedElementState == null) { + val selectedElementEntity = selectedElementEntity.value + + if (selectedElementState == null || selectedElementEntity == null) { return } @@ -222,13 +228,15 @@ class InteractUiElementViewModel( val action = ActionData.InteractUiElement( description = selectedElementState.description, nodeAction = selectedElementState.selectedInteraction, - packageName = selectedElementState.packageName, - text = selectedElementState.nodeText, - contentDescription = selectedElementState.nodeText, - className = selectedElementState.nodeClassName, - viewResourceId = selectedElementState.nodeViewResourceId, - uniqueId = selectedElementState.nodeUniqueId, - nodeActions = selectedElementState.interactionTypes.map { it.first }.toSet(), + packageName = selectedElementEntity.packageName, + text = selectedElementEntity.text, + contentDescription = selectedElementEntity.contentDescription, + tooltip = selectedElementEntity.tooltip, + hint = selectedElementEntity.hint, + className = selectedElementEntity.className, + viewResourceId = selectedElementEntity.viewResourceId, + uniqueId = selectedElementEntity.uniqueId, + nodeActions = selectedElementEntity.actions, ) viewModelScope.launch { @@ -271,6 +279,7 @@ class InteractUiElementViewModel( appIcon = appIcon, nodeText = interaction.text ?: interaction.contentDescription, nodeClassName = interaction.className, + nodeToolTipHint = interaction.tooltip ?: interaction.hint, nodeViewResourceId = interaction.viewResourceId, nodeUniqueId = interaction.uniqueId, interactionTypes = buildInteractionTypeFilterItems(interaction.actions), @@ -339,6 +348,7 @@ class InteractUiElementViewModel( nodeText = node.text ?: node.contentDescription, nodeClassName = node.className, nodeUniqueId = node.uniqueId, + nodeTooltipHint = node.tooltip ?: node.hint, interactionTypesText = node.actions.joinToString { getInteractionTypeString(it) }, interactionTypes = node.actions, interacted = node.interacted, @@ -390,6 +400,7 @@ data class SelectedUiElementState( val appName: String, val appIcon: ComposeIconInfo.Drawable?, val nodeText: String?, + val nodeToolTipHint: String?, val nodeClassName: String?, val nodeViewResourceId: String?, val nodeUniqueId: String?, @@ -419,6 +430,7 @@ data class UiElementListItemModel( val id: Long, val nodeViewResourceId: String?, val nodeText: String?, + val nodeTooltipHint: String?, val nodeClassName: String?, val nodeUniqueId: String?, val interactionTypesText: String, diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index 4385e0b3a4..be62380df3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -95,6 +95,8 @@ data class ActionEntity( const val EXTRA_ACCESSIBILITY_CONTENT_DESCRIPTION = "extra_accessibility_content_description" const val EXTRA_ACCESSIBILITY_TEXT = "extra_accessibility_text" + const val EXTRA_ACCESSIBILITY_TOOLTIP = "extra_accessibility_tooltip" + const val EXTRA_ACCESSIBILITY_HINT = "extra_accessibility_hint" const val EXTRA_ACCESSIBILITY_CLASS_NAME = "extra_accessibility_class_name" const val EXTRA_ACCESSIBILITY_VIEW_RESOURCE_ID = "extra_accessibility_view_resource_id" const val EXTRA_ACCESSIBILITY_UNIQUE_ID = "extra_accessibility_unique_id" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 927a5b57f0..603ef87d6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1147,8 +1147,9 @@ Interaction details Description App - Text / content description + Text/content description Class name + Tooltip/hint View resource ID Unique ID Interaction types From 887a86659f803cb62e48cff60af84c2afcb4c4b1 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 14:10:43 +0200 Subject: [PATCH 55/69] #257 automatically show additional elements if there are none that were interacted with --- .../actions/uielement/InteractUiElementViewModel.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index cc49251b80..7ffd6e993d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -118,6 +119,15 @@ class InteractUiElementViewModel( private val interactionsByPackage: StateFlow>> = selectedApp .filterNotNull() .flatMapLatest { packageName -> useCase.getInteractionsByPackage(packageName) } + .onEach { state -> + // Automatically show additional elements if no elements that were interacted with + // were detected. + state.ifIsData { list -> + if (list.count { it.interacted } == 0) { + showAdditionalElements.update { true } + } + } + } .stateIn(viewModelScope, SharingStarted.Lazily, State.Loading) private val elementListItems: Flow>> = interactionsByPackage @@ -258,6 +268,7 @@ class InteractUiElementViewModel( fun onSelectApp(packageName: String) { elementSearchQuery.update { null } + showAdditionalElements.update { false } selectedApp.update { packageName } } From f1420d68f33297c42330025cc0b752c66e22a3a8 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 14:23:57 +0200 Subject: [PATCH 56/69] #257 automatically fill a description if the node has text or a view resource id --- .../uielement/InteractUiElementScreen.kt | 6 ++---- .../uielement/InteractUiElementViewModel.kt | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 4b680630d9..955bd04fa1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -526,10 +526,9 @@ private fun SelectedElementSection( ) Text(text = state.nodeText, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(8.dp)) - if (state.nodeToolTipHint != null) { Text( text = stringResource(R.string.action_interact_ui_element_tooltip_label), @@ -537,10 +536,9 @@ private fun SelectedElementSection( ) Text(text = state.nodeToolTipHint, style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(8.dp)) - if (state.nodeClassName != null) { Text( text = stringResource(R.string.action_interact_ui_element_class_name_label), diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index 7ffd6e993d..b8e5356157 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -268,7 +268,11 @@ class InteractUiElementViewModel( fun onSelectApp(packageName: String) { elementSearchQuery.update { null } - showAdditionalElements.update { false } + + if (packageName != selectedApp.value) { + showAdditionalElements.update { false } + } + selectedApp.update { packageName } } @@ -282,9 +286,19 @@ class InteractUiElementViewModel( val selectedInteraction = NodeInteractionType.entries.first { interaction.actions.contains(it) } + val interactionText = getInteractionTypeString(selectedInteraction) + val descriptionElement = + interaction.text ?: interaction.contentDescription ?: interaction.tooltip + ?: interaction.hint ?: interaction.viewResourceId + + val description = if (descriptionElement == null) { + "" + } else { + "$interactionText: $descriptionElement" + } val newState = SelectedUiElementState( - description = "", + description = description, packageName = interaction.packageName, appName = appName, appIcon = appIcon, From d03db42aa1c4b282f1c075c33ccc4443035bc31e Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 15:04:40 +0200 Subject: [PATCH 57/69] #257 adapt layouts for screens with a small height --- .idea/codeStyles/Project.xml | 35 +++ .../uielement/ChooseUiElementScreen.kt | 291 ++++++++++++------ .../uielement/InteractUiElementScreen.kt | 187 +++++++---- .../uielement/InteractUiElementViewModel.kt | 1 + 4 files changed, 360 insertions(+), 154 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 9b58552057..17554251a8 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -5,6 +5,41 @@ + + + diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt index 4d262d60f2..340f304358 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/ChooseUiElementScreen.kt @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material3.BottomAppBar @@ -27,6 +29,7 @@ import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,14 +44,18 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.SearchAppBarActions +import io.github.sds100.keymapper.util.ui.compose.WindowSizeClassExt.compareTo @Composable fun ChooseElementScreen( @@ -62,7 +69,9 @@ fun ChooseElementScreen( onSelectInteractionType: (NodeInteractionType?) -> Unit = {}, onAdditionalElementsCheckedChange: (Boolean) -> Unit = {}, ) { - var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val widthSizeClass = windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass + val heightSizeClass = windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass Scaffold( modifier.displayCutoutPadding(), @@ -107,82 +116,137 @@ fun ChooseElementScreen( style = MaterialTheme.typography.titleLarge, ) - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_choose_element_text), - style = MaterialTheme.typography.bodyMedium, - ) - - Spacer(modifier = Modifier.height(8.dp)) + if (heightSizeClass == WindowHeightSizeClass.COMPACT || widthSizeClass >= WindowWidthSizeClass.EXPANDED) { + Row { + InfoSection( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + state = state, + onSelectInteractionType = onSelectInteractionType, + onAdditionalElementsCheckedChange = onAdditionalElementsCheckedChange, + ) - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Rounded.ErrorOutline, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, + ListSection( + modifier = Modifier.weight(1f), + state = state, + onClickElement = onClickElement, + ) + } + } else { + InfoSection( + state = state, + onSelectInteractionType = onSelectInteractionType, + onAdditionalElementsCheckedChange = onAdditionalElementsCheckedChange, ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_subtitle), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleSmall, + + ListSection( + modifier = Modifier.fillMaxSize(), + state = state, + onClickElement = onClickElement, ) } + } + } + } +} - Spacer(modifier = Modifier.height(8.dp)) +@Composable +private fun InfoSection( + modifier: Modifier = Modifier, + state: State, + onSelectInteractionType: (NodeInteractionType?) -> Unit, + onAdditionalElementsCheckedChange: (Boolean) -> Unit, +) { + Column(modifier = modifier) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_text), + style = MaterialTheme.typography.bodyMedium, + ) - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - when (state) { - State.Loading -> LoadingList(modifier = Modifier.fillMaxSize()) - is State.Data -> { - val listItems = state.data.listItems + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.ErrorOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_subtitle), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleSmall, + ) + } - CheckBoxText( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - text = stringResource(R.string.action_interact_ui_element_checkbox_additional_elements), - isChecked = state.data.showAdditionalElements, - onCheckedChange = onAdditionalElementsCheckedChange, - ) + Spacer(modifier = Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(8.dp)) - - if (listItems.isEmpty()) { - EmptyList( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - ) - } else { - KeyMapperDropdownMenu( - modifier = Modifier.padding(horizontal = 16.dp), - expanded = interactionTypeExpanded, - onExpandedChange = { interactionTypeExpanded = it }, - label = { Text(stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown)) }, - values = state.data.interactionTypes, - selectedValue = state.data.selectedInteractionType, - onValueChanged = onSelectInteractionType, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - LoadedList( - modifier = Modifier.fillMaxSize(), - listItems = listItems, - onClick = onClickElement, - ) - } - } + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_choose_element_not_found_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (state is State.Data) { + var interactionTypeExpanded by rememberSaveable { mutableStateOf(false) } + + CheckBoxText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.action_interact_ui_element_checkbox_additional_elements), + isChecked = state.data.showAdditionalElements, + onCheckedChange = onAdditionalElementsCheckedChange, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperDropdownMenu( + modifier = Modifier.padding(horizontal = 16.dp), + expanded = interactionTypeExpanded, + onExpandedChange = { interactionTypeExpanded = it }, + label = { Text(stringResource(R.string.action_interact_ui_element_filter_interaction_type_dropdown)) }, + values = state.data.interactionTypes, + selectedValue = state.data.selectedInteractionType, + onValueChanged = onSelectInteractionType, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun ListSection( + modifier: Modifier = Modifier, + state: State, + onClickElement: (Long) -> Unit, +) { + when (state) { + State.Loading -> LoadingList(modifier = modifier.fillMaxSize()) + is State.Data -> { + val listItems = state.data.listItems + + Column(modifier = modifier) { + if (listItems.isEmpty()) { + EmptyList( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) + } else { + LoadedList( + modifier = Modifier.fillMaxSize(), + listItems = listItems, + onClick = onClickElement, + ) } } } @@ -352,41 +416,74 @@ private fun Loading() { } } +private val listItems = listOf( + UiElementListItemModel( + id = 1L, + nodeText = "Open Settings", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "menu_button", + nodeUniqueId = "123456789", + nodeTooltipHint = "Open menu", + interactionTypesText = "Tap, Tap and hold, Scroll forward", + interactionTypes = setOf( + NodeInteractionType.CLICK, + NodeInteractionType.LONG_CLICK, + NodeInteractionType.SCROLL_FORWARD, + ), + interacted = true, + ), +) + +private val loadedState = SelectUiElementState( + listItems = listItems, + interactionTypes = listOf( + null to "Any", + NodeInteractionType.CLICK to "Tap", + NodeInteractionType.LONG_CLICK to "Tap and hold", + ), + selectedInteractionType = null, + showAdditionalElements = true, +) + @Preview @Composable -private fun Loaded() { - val listItems = listOf( - UiElementListItemModel( - id = 1L, - nodeText = "Open Settings", - nodeClassName = "android.widget.ImageButton", - nodeViewResourceId = "menu_button", - nodeUniqueId = "123456789", - nodeTooltipHint = "Open menu", - interactionTypesText = "Tap, Tap and hold, Scroll forward", - interactionTypes = setOf( - NodeInteractionType.CLICK, - NodeInteractionType.LONG_CLICK, - NodeInteractionType.SCROLL_FORWARD, - ), - interacted = true, - ), - ) +private fun LoadedPortrait() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} - val state = SelectUiElementState( - listItems = listItems, - interactionTypes = listOf( - null to "Any", - NodeInteractionType.CLICK to "Tap", - NodeInteractionType.LONG_CLICK to "Tap and hold", - ), - selectedInteractionType = null, - showAdditionalElements = true, - ) +@Preview(widthDp = 800, heightDp = 300) +@Composable +private fun LoadedPhoneLandscape() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} +@Preview(device = Devices.TABLET) +@Composable +private fun LoadedTablet() { + KeyMapperTheme { + ChooseElementScreen( + state = State.Data(loadedState), + query = "Key Mapper", + ) + } +} + +@Preview(device = Devices.NEXUS_7) +@Composable +private fun LoadedTabletVertical() { KeyMapperTheme { ChooseElementScreen( - state = State.Data(state), + state = State.Data(loadedState), query = "Key Mapper", ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt index 955bd04fa1..7ff879f499 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -42,6 +43,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -57,12 +59,15 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import com.google.accompanist.drawablepainter.rememberDrawablePainter import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme @@ -73,6 +78,7 @@ import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu import io.github.sds100.keymapper.util.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.util.ui.compose.WindowSizeClassExt.compareTo import io.github.sds100.keymapper.util.ui.compose.icons.AdGroup import io.github.sds100.keymapper.util.ui.compose.icons.JumpToElement import io.github.sds100.keymapper.util.ui.compose.icons.KeyMapperIcons @@ -181,6 +187,10 @@ private fun LandingScreen( ) { val snackbarHostState = SnackbarHostState() + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val widthSizeClass = windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass + val heightSizeClass = windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass + Scaffold( modifier.displayCutoutPadding(), snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -225,7 +235,7 @@ private fun LandingScreen( end = endPadding, ), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Column { Text( modifier = Modifier.padding( start = 16.dp, @@ -237,44 +247,91 @@ private fun LandingScreen( style = MaterialTheme.typography.titleLarge, ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = stringResource(R.string.action_interact_ui_element_description), - style = MaterialTheme.typography.bodyMedium, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - RecordingSection( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = recordState, - onRecordClick = onRecordClick, - openSelectAppScreen = openSelectAppScreen, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - if (selectedElementState != null) { - HorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - SelectedElementSection( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = selectedElementState, - onSelectInteractionType = onSelectInteractionType, - onDescriptionChanged = onDescriptionChanged, - ) + if (heightSizeClass == WindowHeightSizeClass.COMPACT || widthSizeClass >= WindowWidthSizeClass.EXPANDED) { + Row { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxHeight() + .weight(1f), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + RecordingSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = recordState, + onRecordClick = onRecordClick, + openSelectAppScreen = openSelectAppScreen, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + if (selectedElementState != null) { + SelectedElementSection( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxHeight() + .padding(horizontal = 16.dp) + .weight(1f), + state = selectedElementState, + onSelectInteractionType = onSelectInteractionType, + onDescriptionChanged = onDescriptionChanged, + ) + } + } + } else { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource(R.string.action_interact_ui_element_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + RecordingSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = recordState, + onRecordClick = onRecordClick, + openSelectAppScreen = openSelectAppScreen, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (selectedElementState != null) { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SelectedElementSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = selectedElementState, + onSelectInteractionType = onSelectInteractionType, + onDescriptionChanged = onDescriptionChanged, + ) + } + } } } } @@ -609,36 +666,52 @@ private fun PreviewEmpty() { @Preview @Composable -private fun PreviewSelectedElement() { +private fun PreviewLoading() { + KeyMapperTheme { + LandingScreen( + recordState = State.Loading, + selectedElementState = null, + ) + } +} + +@Composable +private fun selectedUiElementState(): SelectedUiElementState { val appIcon = LocalContext.current.drawable(R.mipmap.ic_launcher_round) + return SelectedUiElementState( + description = "Tap test node", + packageName = "com.example.test", + appName = "Test App", + appIcon = ComposeIconInfo.Drawable(appIcon), + nodeText = "Test Node", + nodeToolTipHint = "Test tooltip", + nodeClassName = "android.widget.ImageButton", + nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", + nodeUniqueId = "123", + interactionTypes = listOf(NodeInteractionType.LONG_CLICK to "Tap and hold"), + selectedInteraction = NodeInteractionType.LONG_CLICK, + ) +} + +@Preview(device = Devices.PIXEL_7) +@Composable +private fun PreviewSelectedElementPortrait() { KeyMapperTheme { LandingScreen( recordState = State.Data(RecordUiElementState.Recorded(3)), - selectedElementState = SelectedUiElementState( - description = "Tap test node", - packageName = "com.example.test", - appName = "Test App", - appIcon = ComposeIconInfo.Drawable(appIcon), - nodeText = "Test Node", - nodeToolTipHint = "Test tooltip", - nodeClassName = "android.widget.ImageButton", - nodeViewResourceId = "io.github.sds100.keymapper:id/menu_button", - nodeUniqueId = "123", - interactionTypes = listOf(NodeInteractionType.LONG_CLICK to "Tap and hold"), - selectedInteraction = NodeInteractionType.LONG_CLICK, - ), + selectedElementState = selectedUiElementState(), ) } } -@Preview +@Preview(widthDp = 800, heightDp = 300) @Composable -private fun PreviewLoading() { +private fun PreviewSelectedElementLandscape() { KeyMapperTheme { LandingScreen( - recordState = State.Loading, - selectedElementState = null, + recordState = State.Data(RecordUiElementState.Recorded(3)), + selectedElementState = selectedUiElementState(), ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt index b8e5356157..67dcac391c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/uielement/InteractUiElementViewModel.kt @@ -311,6 +311,7 @@ class InteractUiElementViewModel( selectedInteraction = selectedInteraction, ) + selectedElementEntity.update { interaction } _selectedElementState.update { newState } } } From 9855cc780ae21fea8c5557c38eca9f833c900766 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 15:08:19 +0200 Subject: [PATCH 58/69] #1682 feat: show "Purchased!" text next to the use button for advanced triggers --- CHANGELOG.md | 1 + app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 029b253347..54fbf2380f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - #699 Time constraints ⏰ - #257 Action to interact with user interface elements inside other apps. - #1663 Actions to stop, step forward, and step backward playing media. +- #1682 Show "Purchased!" text next to the use button for advanced triggers. ## Changed diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 603ef87d6f..20ee36a04c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1283,6 +1283,7 @@ Unlock (%s) Use Loading… + Purchased! Retry fetching price Purchase cancelled. This requires a paid feature that can only be bought by downloading Key Mapper from Google Play. From e7b34034a1b4af06d084cc28100a6baf66c841b3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 15:19:01 +0200 Subject: [PATCH 59/69] #1684 fix: removed the redundant and broken refresh devices button when configuring a key event action because they are automatically refreshed anyway --- CHANGELOG.md | 1 + .../keyevent/ConfigKeyEventActionFragment.kt | 6 ----- .../keyevent/ConfigKeyEventActionViewModel.kt | 12 ---------- .../system/devices/AndroidDevicesAdapter.kt | 8 +++---- .../res/layout/fragment_config_key_event.xml | 22 +++++-------------- 5 files changed, 10 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54fbf2380f..6608ffd78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - #1683 key event actions work in Minecraft and other apps again. - Export log files as .txt instead of .zip files. +- #1684 Removed the redundant and broken refresh devices button when configuring a key event action because they are automatically refreshed anyway. ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt index 6f90c1ceaa..d674a421df 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt @@ -175,12 +175,6 @@ class ConfigKeyEventActionFragment : Fragment() { } } - override fun onResume() { - super.onResume() - - viewModel.rebuildUiState() - } - override fun onDestroyView() { _binding = null super.onDestroyView() diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt index e6734d0921..c28b02a48a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import splitties.bitflags.hasFlag import splitties.bitflags.minusFlag import splitties.bitflags.withFlag @@ -63,11 +62,8 @@ class ConfigKeyEventActionViewModel( private val _returnResult = MutableSharedFlow() val returnResult = _returnResult.asSharedFlow() - private val rebuildUiState = MutableSharedFlow() - init { viewModelScope.launch { - combine( keyEventState, useCase.inputDevices, @@ -178,14 +174,6 @@ class ConfigKeyEventActionViewModel( } } - fun refreshDevices() { - rebuildUiState() - } - - fun rebuildUiState() { - runBlocking { rebuildUiState.emit(Unit) } - } - private fun buildUiState( state: KeyEventState, inputDeviceList: List, diff --git a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index daa1799a9a..f593683c97 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -125,8 +125,8 @@ class AndroidDevicesAdapter( } override fun getInputDeviceName(descriptor: String): Result { - InputDevice.getDeviceIds().forEach { - val device = InputDevice.getDevice(it) ?: return@forEach + for (id in InputDevice.getDeviceIds()) { + val device = InputDevice.getDevice(id) ?: continue if (device.descriptor == descriptor) { return Success(device.name) @@ -139,8 +139,8 @@ class AndroidDevicesAdapter( private fun updateInputDevices() { val devices = mutableListOf() - InputDevice.getDeviceIds().forEach { - val device = InputDevice.getDevice(it) ?: return@forEach + for (id in InputDevice.getDeviceIds()) { + val device = InputDevice.getDevice(id) ?: continue devices.add(InputDeviceUtils.createInputDeviceInfo(device)) } diff --git a/app/src/main/res/layout/fragment_config_key_event.xml b/app/src/main/res/layout/fragment_config_key_event.xml index 87b7853f12..c9947104c9 100644 --- a/app/src/main/res/layout/fragment_config_key_event.xml +++ b/app/src/main/res/layout/fragment_config_key_event.xml @@ -28,7 +28,9 @@ android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginBottom="@dimen/bottom_app_bar_height"> + android:layout_marginBottom="@dimen/bottom_app_bar_height" + app:layout_anchor="@+id/scrollView" + app:layout_anchorGravity="center"> @@ -126,20 +128,6 @@ - - Date: Fri, 9 May 2025 15:22:06 +0200 Subject: [PATCH 60/69] #1684 fix: attempt to fix the list of devices going empty randomly --- .../keyevent/ConfigKeyEventActionViewModel.kt | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt index c28b02a48a..c514e7a2c1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -27,11 +27,12 @@ import io.github.sds100.keymapper.util.ui.navigate import io.github.sds100.keymapper.util.valueOrNull import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import splitties.bitflags.hasFlag import splitties.bitflags.minusFlag @@ -50,32 +51,25 @@ class ConfigKeyEventActionViewModel( private val keyEventState = MutableStateFlow(KeyEventState()) - private val _uiState = MutableStateFlow( + val uiState: StateFlow = combine( + keyEventState, + useCase.inputDevices, + useCase.showDeviceDescriptors, + ) { state, inputDevices, showDeviceDescriptors -> + buildUiState(state, inputDevices, showDeviceDescriptors) + }.stateIn( + viewModelScope, + SharingStarted.Lazily, buildUiState( keyEventState.value, inputDeviceList = emptyList(), showDeviceDescriptors = false, ), ) - val uiState = _uiState.asStateFlow() private val _returnResult = MutableSharedFlow() val returnResult = _returnResult.asSharedFlow() - init { - viewModelScope.launch { - combine( - keyEventState, - useCase.inputDevices, - useCase.showDeviceDescriptors, - ) { state, inputDevices, showDeviceDescriptors -> - buildUiState(state, inputDevices, showDeviceDescriptors) - }.collectLatest { - _uiState.value = it - } - } - } - fun setModifierKeyChecked(modifier: Int, isChecked: Boolean) { val oldMetaState = keyEventState.value.metaState From 0c4f4abf09097e4796091bba9199d5731d39b8c4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 17:43:35 +0200 Subject: [PATCH 61/69] #1687 fix: restoring key map groups would sometimes fail due to improper breadth first traversal of the group tree --- CHANGELOG.md | 1 + .../sds100/keymapper/backup/BackupManager.kt | 139 ++++++++++++------ .../github/sds100/keymapper/util/TreeNode.kt | 16 ++ .../sds100/keymapper/BackupManagerTest.kt | 57 ++++++- 4 files changed, 164 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/TreeNode.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6608ffd78b..89a7a0807b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - #1683 key event actions work in Minecraft and other apps again. - Export log files as .txt instead of .zip files. - #1684 Removed the redundant and broken refresh devices button when configuring a key event action because they are automatically refreshed anyway. +- #1687 restoring key map groups would sometimes fail. ## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index 8e524a033b..d56aaf4d19 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -54,7 +54,9 @@ import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.Success +import io.github.sds100.keymapper.util.TreeNode import io.github.sds100.keymapper.util.UuidGenerator +import io.github.sds100.keymapper.util.breadFirstTraversal import io.github.sds100.keymapper.util.onFailure import io.github.sds100.keymapper.util.then import kotlinx.coroutines.CoroutineScope @@ -473,55 +475,11 @@ class BackupManagerImpl( // Group parents must be restored first so an SqliteConstraintException // is not thrown when restoring a child group. - val groupsToRestoreMap = backupContent.groups.associateBy { it.uid }.toMutableMap() - val groupRestoreQueue = LinkedList() + val groupRestoreTrees = buildGroupTrees(backupContent.groups) - // Order the groups into a queue such that a parent is always before a child. - for (group in backupContent.groups) { - if (groupsToRestoreMap.containsKey(group.uid)) { - groupRestoreQueue.addFirst(group) - } - - var parent = groupsToRestoreMap[group.parentUid] - - while (parent != null) { - groupRestoreQueue.addFirst(parent) - groupsToRestoreMap.remove(parent.uid) - parent = groupsToRestoreMap[parent.parentUid] - } - } - - for (group in groupRestoreQueue) { - // Set the last opened date to now so that the imported group - // shows as the most recent. - var modifiedGroup = group.copy(lastOpenedDate = currentTime) - - // If the group's parent wasn't backed up or doesn't exist - // then set it the parent to the root group - if (!groupUids.contains(group.parentUid)) { - modifiedGroup = modifiedGroup.copy(parentUid = null) - } - - val siblings = - groupRepository.getGroupsByParent(modifiedGroup.parentUid).first() - - modifiedGroup = RepositoryUtils.saveUniqueName( - modifiedGroup, - saveBlock = { renamedGroup -> - // Do not rename the group with a (1) if it is the same UID. Just overwrite the name. - if (siblings.any { sibling -> sibling.uid != renamedGroup.uid && sibling.name == renamedGroup.name }) { - throw IllegalStateException("Non unique group name") - } - }, - renameBlock = { entity, suffix -> - entity.copy(name = "${entity.name} $suffix") - }, - ) - - if (existingGroupUids.contains(modifiedGroup.uid)) { - groupRepository.update(modifiedGroup) - } else { - groupRepository.insert(modifiedGroup) + for (tree in groupRestoreTrees) { + tree.breadFirstTraversal { group -> + restoreGroup(group, currentTime, groupUids, existingGroupUids) } } } @@ -615,6 +573,91 @@ class BackupManagerImpl( } } + private suspend fun restoreGroup( + group: GroupEntity, + currentTime: Long, + groupUids: Set, + existingGroupUids: Set, + ) { + // Set the last opened date to now so that the imported group + // shows as the most recent. + var modifiedGroup = group.copy(lastOpenedDate = currentTime) + + // If the group's parent wasn't backed up or doesn't exist + // then set it the parent to the root group + if (!groupUids.contains(group.parentUid)) { + modifiedGroup = modifiedGroup.copy(parentUid = null) + } + + val siblings = + groupRepository.getGroupsByParent(modifiedGroup.parentUid).first() + + modifiedGroup = RepositoryUtils.saveUniqueName( + modifiedGroup, + saveBlock = { renamedGroup -> + // Do not rename the group with a (1) if it is the same UID. Just overwrite the name. + if (siblings.any { sibling -> sibling.uid != renamedGroup.uid && sibling.name == renamedGroup.name }) { + throw IllegalStateException("Non unique group name") + } + }, + renameBlock = { entity, suffix -> + entity.copy(name = "${entity.name} $suffix") + }, + ) + + if (existingGroupUids.contains(modifiedGroup.uid)) { + groupRepository.update(modifiedGroup) + } else { + groupRepository.insert(modifiedGroup) + } + } + + /** + * Converts the group relationships into trees. This first finds all the root groups which + * have no parent. Then it loops over all the other groups indefinitely until they have been + * added to their parent. If the parent does not exist while looping then it is skipped and + * processed in the next iteration. + * + * @return A list of the root nodes for all the group trees. + */ + private fun buildGroupTrees(groups: List): List> { + if (groups.isEmpty()) { + return emptyList() + } + + val nodeMap = mutableMapOf>() + val rootNodes = mutableListOf>() + + val groupQueue = LinkedList() + + for (group in groups) { + if (group.parentUid == null) { + val node = TreeNode(group) + nodeMap[group.uid] = node + rootNodes.add(node) + } else { + groupQueue.add(group) + } + } + + while (groupQueue.isNotEmpty()) { + val groupsToRemove = mutableListOf() + + for (group in groupQueue) { + if (nodeMap.containsKey(group.parentUid)) { + val node = TreeNode(group) + nodeMap[group.uid] = node + nodeMap[group.parentUid]!!.children.add(node) + groupsToRemove.add(group) + } + } + + groupQueue.removeAll(groupsToRemove.toSet()) + } + + return rootNodes + } + private suspend fun appendKeyMapsInRepository(keyMaps: List) = withContext(dispatchers.default()) { val randomUids = keyMaps.map { it.copy(uid = UUID.randomUUID().toString()) } keyMapRepository.insert(*randomUids.toTypedArray()) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/TreeNode.kt b/app/src/main/java/io/github/sds100/keymapper/util/TreeNode.kt new file mode 100644 index 0000000000..52e63d0e30 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/TreeNode.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.util + +data class TreeNode(val value: T, val children: MutableList> = mutableListOf()) + +inline fun TreeNode.breadFirstTraversal( + action: (T) -> Unit, +) { + val queue = ArrayDeque>() + queue.add(this) + + while (queue.isNotEmpty()) { + val currentNode = queue.removeFirst() + action(currentNode.value) + queue.addAll(currentNode.children) + } +} diff --git a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt index a76df90b1a..287e83fde1 100644 --- a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt @@ -136,6 +136,61 @@ class BackupManagerTest { Dispatchers.resetMain() } + /** + * Issue #1655. If the list of groups in the backup has a child before the parent then the + * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. + */ + @Test + fun `restore groups breadth first so parents exist before children are restored with child first in the backup`() = runTest(testDispatcher) { + val parentGroup1 = GroupEntity( + uid = "parent_group_1_uid", + name = "parent_group_1_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val parentGroup2 = GroupEntity( + uid = "parent_group_2_uid", + name = "parent_group_2_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val childGroup = GroupEntity( + uid = "child_group_uid", + name = "child_group_name", + parentUid = parentGroup1.uid, + lastOpenedDate = 0L, + ) + + val grandChildGroup = GroupEntity( + uid = "grand_child_group_uid", + name = "grand_child_group_name", + parentUid = childGroup.uid, + lastOpenedDate = 0L, + ) + + val backupContent = BackupContent( + appVersion = Constants.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(childGroup, grandChildGroup, parentGroup1), + ) + + inOrder(mockGroupRepository) { + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, + ) + + verify(mockGroupRepository).insert(parentGroup1) + verify(mockGroupRepository).insert(childGroup) + verify(mockGroupRepository).insert(grandChildGroup) + verify(mockGroupRepository, never()).update(any()) + } + } + /** * Issue #1655. If the list of groups in the backup has a child before the parent then the * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. @@ -184,10 +239,10 @@ class BackupManagerTest { currentTime = 0L, ) + verify(mockGroupRepository).insert(parentGroup2) verify(mockGroupRepository).insert(parentGroup1) verify(mockGroupRepository).insert(childGroup) verify(mockGroupRepository).insert(grandChildGroup) - verify(mockGroupRepository).insert(parentGroup2) verify(mockGroupRepository, never()).update(any()) } } From d9462bd721b42d7b943d96cb5457698de1aa5847 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 17:55:04 +0200 Subject: [PATCH 62/69] fix: update datastore library to try and fix the InvalidProtocolBufferException --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e442585f81..13f120a407 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -225,7 +225,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.viewpager2:viewpager2:1.1.0" - implementation "androidx.datastore:datastore-preferences:1.2.0-alpha01" + implementation "androidx.datastore:datastore-preferences:1.2.0-alpha02" implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.activity:activity-compose:1.10.1" implementation "androidx.navigation:navigation-compose:2.8.9" From 162651ae8c29c9dc5f488888ec08af8b9425891f Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 9 May 2025 17:56:03 +0200 Subject: [PATCH 63/69] #1688 fix: do not crash when inserting key maps if they already exist --- .../java/io/github/sds100/keymapper/actions/Action.kt | 10 +++------- .../keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt | 8 +++++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt b/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt index df4eccb63b..b5a2a5db4e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/Action.kt @@ -33,11 +33,7 @@ data class Action( val multiplier: Int? = null, val delayBeforeNextAction: Int? = null, -) { - companion object { - const val REPEAT_DELAY_MIN = 0 - } -} +) object ActionEntityMapper { fun fromEntity(entity: ActionEntity): Action? { @@ -108,7 +104,7 @@ object ActionEntityMapper { ) } - fun toEntity(keyMap: KeyMap): List = keyMap.actionList.mapNotNull { action -> + fun toEntity(keyMap: KeyMap): List = keyMap.actionList.map { action -> val base = ActionDataEntityMapper.toEntity(action.data) val extras = mutableListOf().apply { @@ -187,7 +183,7 @@ object ActionEntityMapper { flags = flags.withFlag(ActionEntity.ACTION_FLAG_HOLD_DOWN) } - return@mapNotNull ActionEntity( + return@map ActionEntity( type = base.type, data = base.data, extras = base.extras.plus(extras), diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt index 5f69f2bff7..842264bfea 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.mappings.keymaps +import android.database.sqlite.SQLiteConstraintException import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.actions.RepeatMode @@ -851,7 +852,12 @@ class ConfigKeyMapUseCaseController( val keyMap = keyMap.value.dataOrNull() ?: return if (keyMap.dbId == null) { - keyMapRepository.insert(KeyMapEntityMapper.toEntity(keyMap, 0)) + val entity = KeyMapEntityMapper.toEntity(keyMap, 0) + try { + keyMapRepository.insert(entity) + } catch (e: SQLiteConstraintException) { + keyMapRepository.update(entity) + } } else { keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) } From 6f2a6f731c4529fbb3e6aa016cb0c7d1fd50eb38 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 10 May 2025 11:20:54 +0200 Subject: [PATCH 64/69] #1684 fix: devices in config key event screen are cleared --- .../keyevent/ConfigKeyEventActionFragment.kt | 88 ++++++++++--------- .../res/layout/fragment_config_key_event.xml | 1 - 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt index d674a421df..1f6dfcf81d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionFragment.kt @@ -51,6 +51,14 @@ class ConfigKeyEventActionFragment : Fragment() { val binding: FragmentConfigKeyEventBinding get() = _binding!! + private val deviceArrayAdapter: ArrayAdapter by lazy { + ArrayAdapter( + requireContext(), + R.layout.dropdown_menu_popup_item, + mutableListOf(), + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,47 +106,9 @@ class ConfigKeyEventActionFragment : Fragment() { findNavController().navigateUp() } - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.returnResult.collectLatest { - setFragmentResult( - requestKey, - Bundle().apply { putJsonSerializable(EXTRA_RESULT, it) }, - ) - - findNavController().navigateUp() - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.uiState.collectLatest { state -> - binding.epoxyRecyclerViewModifiers.withModels { - state.modifierListItems.forEach { listItem -> - configuredCheckBox(listItem) { isChecked -> - viewModel.setModifierKeyChecked(listItem.id.toInt(), isChecked) - } - } - } - - ArrayAdapter( - requireContext(), - R.layout.dropdown_menu_popup_item, - mutableListOf(), - ).apply { - clear() - add(str(R.string.from_no_device)) - - state.deviceListItems.forEach { - add(it.name) - } - - binding.dropdownDeviceId.setAdapter(this) - } - - binding.textInputLayoutKeyCode.error = state.keyCodeErrorMessage - } - } - binding.dropdownDeviceId.apply { + setAdapter(deviceArrayAdapter) + // set the default value setText(str(R.string.from_no_device), false) @@ -173,6 +143,44 @@ class ConfigKeyEventActionFragment : Fragment() { } } } + + viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.returnResult.collectLatest { + setFragmentResult( + requestKey, + Bundle().apply { putJsonSerializable(EXTRA_RESULT, it) }, + ) + + findNavController().navigateUp() + } + } + + viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.uiState.collect { state -> + binding.epoxyRecyclerViewModifiers.withModels { + state.modifierListItems.forEach { listItem -> + configuredCheckBox(listItem) { isChecked -> + viewModel.setModifierKeyChecked(listItem.id.toInt(), isChecked) + } + } + } + + deviceArrayAdapter.apply { + clear() + add(str(R.string.from_no_device)) + for (device in state.deviceListItems) { + add(device.name) + } + notifyDataSetChanged() + } + + // Filtering must be false so that the dropdown items aren't cleared + // when setting text. + binding.dropdownDeviceId.setText(state.chosenDeviceName, false) + + binding.textInputLayoutKeyCode.error = state.keyCodeErrorMessage + } + } } override fun onDestroyView() { diff --git a/app/src/main/res/layout/fragment_config_key_event.xml b/app/src/main/res/layout/fragment_config_key_event.xml index c9947104c9..41011c8334 100644 --- a/app/src/main/res/layout/fragment_config_key_event.xml +++ b/app/src/main/res/layout/fragment_config_key_event.xml @@ -123,7 +123,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="none" - android:text="@{viewModel.uiState.chosenDeviceName}" tools:ignore="LabelFor" /> From f4b1b05565a317a568cd1b035756b6e533476615 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 10 May 2025 11:21:07 +0200 Subject: [PATCH 65/69] chore: bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 1085b82907..2beb635ef1 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.1.0 -VERSION_CODE=114 +VERSION_CODE=115 VERSION_NUM=0 \ No newline at end of file From d846114608ac1911a68e8c765d5e67e6293706de Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 10 May 2025 09:21:34 +0000 Subject: [PATCH 66/69] New Crowdin translations by GitHub Action --- app/src/main/res/values-tr/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 700903f43f..90b10fd127 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -21,7 +21,6 @@ Erişilebilirlik servisini yeniden başlat Paylaş Burada hiçbir şey yok! - Burada hiçbir şey yok! Tekrarlamayı durdur… Tetikleyici bırakıldığında Tetikleyici tekrar basıldığında @@ -893,7 +892,6 @@ Etkileşim detayları Açıklama Uygulama - Metin / içerik açıklaması Sınıf adı Kaynak kimliğini görüntüle Özgün kimlik From f1cffed78889c62cbab0ba31e370f04863451d77 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 10 May 2025 11:22:30 +0200 Subject: [PATCH 67/69] chore: update changelog and whats new --- CHANGELOG.md | 2 +- app/src/main/assets/whats-new.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a7a0807b..d71394fc8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [3.1.0](https://github.com/sds100/KeyMapper/releases/tag/v3.1.0) -#### TO BE RELEASED +#### 10 May 2025 ## Added diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index d1de1ca498..d2b07cf8ea 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1,9 +1,9 @@ +Fix for Minecraft 1.21.80! + ⏰ Time constraints. 🔎 Action to interact with app elements. -Fix for Minecraft 1.21.80. - == 3.0 features == 🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. From aad848afac4887c4889a256af575784b5dffb9db Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 10 May 2025 11:27:06 +0200 Subject: [PATCH 68/69] fix test for ConfigKeyEventActionViewModel --- .../keyevent/ConfigKeyEventActionViewModel.kt | 18 ++++++++---------- ...ConfigKeyServiceEventActionViewModelTest.kt | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt index c514e7a2c1..30d1236f18 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -59,7 +59,7 @@ class ConfigKeyEventActionViewModel( buildUiState(state, inputDevices, showDeviceDescriptors) }.stateIn( viewModelScope, - SharingStarted.Lazily, + SharingStarted.Eagerly, buildUiState( keyEventState.value, inputDeviceList = emptyList(), @@ -133,17 +133,15 @@ class ConfigKeyEventActionViewModel( } fun chooseDevice(index: Int) { - viewModelScope.launch { - val chosenDevice = uiState.value.deviceListItems.getOrNull(index) - - if (chosenDevice == null) { - return@launch - } + val chosenDevice = uiState.value.deviceListItems.getOrNull(index) - keyEventState.value = keyEventState.value.copy( - chosenDevice = chosenDevice, - ) + if (chosenDevice == null) { + return } + + keyEventState.value = keyEventState.value.copy( + chosenDevice = chosenDevice, + ) } fun onDoneClick() { diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt index e28c7f0db1..298c1dc130 100644 --- a/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt @@ -46,7 +46,7 @@ class ConfigKeyServiceEventActionViewModelTest { @Before fun init() { Dispatchers.setMain(testDispatcher) - inputDevices = MutableStateFlow(emptyList()) + inputDevices = MutableStateFlow(emptyList()) mockUseCase = mock { on { showDeviceDescriptors }.then { MutableStateFlow(false) } From 301f108b2bad4d4fff3bf155af05aa79e2105ae2 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sat, 10 May 2025 11:28:34 +0200 Subject: [PATCH 69/69] fastlane: delete redundant translations --- .../metadata/android/ar/full_description.txt | 55 ------------------- .../metadata/android/ar/short_description.txt | 1 - fastlane/metadata/android/ar/title.txt | 1 - .../android/cs_CZ/full_description.txt | 55 ------------------- .../android/cs_CZ/short_description.txt | 1 - fastlane/metadata/android/cs_CZ/title.txt | 1 - .../android/de_DE/full_description.txt | 55 ------------------- .../android/de_DE/short_description.txt | 1 - fastlane/metadata/android/de_DE/title.txt | 1 - .../android/es_ES/full_description.txt | 55 ------------------- .../android/es_ES/short_description.txt | 1 - fastlane/metadata/android/es_ES/title.txt | 1 - .../android/fr_FR/full_description.txt | 55 ------------------- .../android/fr_FR/short_description.txt | 1 - fastlane/metadata/android/fr_FR/title.txt | 1 - .../android/hu_HU/full_description.txt | 55 ------------------- .../android/hu_HU/short_description.txt | 1 - fastlane/metadata/android/hu_HU/title.txt | 1 - .../android/id_ID/full_description.txt | 55 ------------------- .../android/id_ID/short_description.txt | 1 - fastlane/metadata/android/id_ID/title.txt | 1 - .../android/ka_GE/full_description.txt | 55 ------------------- .../android/ka_GE/short_description.txt | 1 - fastlane/metadata/android/ka_GE/title.txt | 1 - .../android/ko_KR/full_description.txt | 55 ------------------- .../android/ko_KR/short_description.txt | 1 - fastlane/metadata/android/ko_KR/title.txt | 1 - .../android/pl_PL/full_description.txt | 55 ------------------- .../android/pl_PL/short_description.txt | 1 - fastlane/metadata/android/pl_PL/title.txt | 1 - .../android/pt_BR/full_description.txt | 55 ------------------- .../android/pt_BR/short_description.txt | 1 - fastlane/metadata/android/pt_BR/title.txt | 1 - .../android/ru_RU/full_description.txt | 55 ------------------- .../android/ru_RU/short_description.txt | 1 - fastlane/metadata/android/ru_RU/title.txt | 1 - .../metadata/android/sk/full_description.txt | 55 ------------------- .../metadata/android/sk/short_description.txt | 1 - fastlane/metadata/android/sk/title.txt | 1 - .../metadata/android/uk/full_description.txt | 55 ------------------- .../metadata/android/uk/short_description.txt | 1 - fastlane/metadata/android/uk/title.txt | 1 - .../metadata/android/vi/full_description.txt | 55 ------------------- .../metadata/android/vi/short_description.txt | 1 - fastlane/metadata/android/vi/title.txt | 1 - .../android/zh_CN/full_description.txt | 55 ------------------- .../android/zh_CN/short_description.txt | 1 - fastlane/metadata/android/zh_CN/title.txt | 1 - .../android/zh_TW/full_description.txt | 55 ------------------- .../android/zh_TW/short_description.txt | 1 - fastlane/metadata/android/zh_TW/title.txt | 1 - 51 files changed, 969 deletions(-) delete mode 100644 fastlane/metadata/android/ar/full_description.txt delete mode 100644 fastlane/metadata/android/ar/short_description.txt delete mode 100644 fastlane/metadata/android/ar/title.txt delete mode 100644 fastlane/metadata/android/cs_CZ/full_description.txt delete mode 100644 fastlane/metadata/android/cs_CZ/short_description.txt delete mode 100644 fastlane/metadata/android/cs_CZ/title.txt delete mode 100644 fastlane/metadata/android/de_DE/full_description.txt delete mode 100644 fastlane/metadata/android/de_DE/short_description.txt delete mode 100644 fastlane/metadata/android/de_DE/title.txt delete mode 100644 fastlane/metadata/android/es_ES/full_description.txt delete mode 100644 fastlane/metadata/android/es_ES/short_description.txt delete mode 100644 fastlane/metadata/android/es_ES/title.txt delete mode 100644 fastlane/metadata/android/fr_FR/full_description.txt delete mode 100644 fastlane/metadata/android/fr_FR/short_description.txt delete mode 100644 fastlane/metadata/android/fr_FR/title.txt delete mode 100644 fastlane/metadata/android/hu_HU/full_description.txt delete mode 100644 fastlane/metadata/android/hu_HU/short_description.txt delete mode 100644 fastlane/metadata/android/hu_HU/title.txt delete mode 100644 fastlane/metadata/android/id_ID/full_description.txt delete mode 100644 fastlane/metadata/android/id_ID/short_description.txt delete mode 100644 fastlane/metadata/android/id_ID/title.txt delete mode 100644 fastlane/metadata/android/ka_GE/full_description.txt delete mode 100644 fastlane/metadata/android/ka_GE/short_description.txt delete mode 100644 fastlane/metadata/android/ka_GE/title.txt delete mode 100644 fastlane/metadata/android/ko_KR/full_description.txt delete mode 100644 fastlane/metadata/android/ko_KR/short_description.txt delete mode 100644 fastlane/metadata/android/ko_KR/title.txt delete mode 100644 fastlane/metadata/android/pl_PL/full_description.txt delete mode 100644 fastlane/metadata/android/pl_PL/short_description.txt delete mode 100644 fastlane/metadata/android/pl_PL/title.txt delete mode 100644 fastlane/metadata/android/pt_BR/full_description.txt delete mode 100644 fastlane/metadata/android/pt_BR/short_description.txt delete mode 100644 fastlane/metadata/android/pt_BR/title.txt delete mode 100644 fastlane/metadata/android/ru_RU/full_description.txt delete mode 100644 fastlane/metadata/android/ru_RU/short_description.txt delete mode 100644 fastlane/metadata/android/ru_RU/title.txt delete mode 100644 fastlane/metadata/android/sk/full_description.txt delete mode 100644 fastlane/metadata/android/sk/short_description.txt delete mode 100644 fastlane/metadata/android/sk/title.txt delete mode 100644 fastlane/metadata/android/uk/full_description.txt delete mode 100644 fastlane/metadata/android/uk/short_description.txt delete mode 100644 fastlane/metadata/android/uk/title.txt delete mode 100644 fastlane/metadata/android/vi/full_description.txt delete mode 100644 fastlane/metadata/android/vi/short_description.txt delete mode 100644 fastlane/metadata/android/vi/title.txt delete mode 100644 fastlane/metadata/android/zh_CN/full_description.txt delete mode 100644 fastlane/metadata/android/zh_CN/short_description.txt delete mode 100644 fastlane/metadata/android/zh_CN/title.txt delete mode 100644 fastlane/metadata/android/zh_TW/full_description.txt delete mode 100644 fastlane/metadata/android/zh_TW/short_description.txt delete mode 100644 fastlane/metadata/android/zh_TW/title.txt diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/ar/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ar/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/ar/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/full_description.txt b/fastlane/metadata/android/cs_CZ/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/cs_CZ/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/short_description.txt b/fastlane/metadata/android/cs_CZ/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/cs_CZ/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/title.txt b/fastlane/metadata/android/cs_CZ/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/cs_CZ/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/full_description.txt b/fastlane/metadata/android/de_DE/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/de_DE/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/short_description.txt b/fastlane/metadata/android/de_DE/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/de_DE/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/title.txt b/fastlane/metadata/android/de_DE/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/de_DE/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/full_description.txt b/fastlane/metadata/android/es_ES/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/es_ES/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/short_description.txt b/fastlane/metadata/android/es_ES/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/es_ES/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/title.txt b/fastlane/metadata/android/es_ES/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/es_ES/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/full_description.txt b/fastlane/metadata/android/fr_FR/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/fr_FR/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/short_description.txt b/fastlane/metadata/android/fr_FR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/fr_FR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/title.txt b/fastlane/metadata/android/fr_FR/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/fr_FR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/full_description.txt b/fastlane/metadata/android/hu_HU/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/hu_HU/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/short_description.txt b/fastlane/metadata/android/hu_HU/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/hu_HU/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/title.txt b/fastlane/metadata/android/hu_HU/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/hu_HU/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/full_description.txt b/fastlane/metadata/android/id_ID/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/id_ID/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/short_description.txt b/fastlane/metadata/android/id_ID/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/id_ID/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/id_ID/title.txt b/fastlane/metadata/android/id_ID/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/id_ID/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/full_description.txt b/fastlane/metadata/android/ka_GE/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/ka_GE/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/short_description.txt b/fastlane/metadata/android/ka_GE/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ka_GE/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/title.txt b/fastlane/metadata/android/ka_GE/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/ka_GE/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/full_description.txt b/fastlane/metadata/android/ko_KR/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/ko_KR/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/short_description.txt b/fastlane/metadata/android/ko_KR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ko_KR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/title.txt b/fastlane/metadata/android/ko_KR/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/ko_KR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/full_description.txt b/fastlane/metadata/android/pl_PL/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/pl_PL/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/short_description.txt b/fastlane/metadata/android/pl_PL/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/pl_PL/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/title.txt b/fastlane/metadata/android/pl_PL/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/pl_PL/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/full_description.txt b/fastlane/metadata/android/pt_BR/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/pt_BR/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/short_description.txt b/fastlane/metadata/android/pt_BR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/pt_BR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/title.txt b/fastlane/metadata/android/pt_BR/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/pt_BR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/full_description.txt b/fastlane/metadata/android/ru_RU/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/ru_RU/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/short_description.txt b/fastlane/metadata/android/ru_RU/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ru_RU/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/title.txt b/fastlane/metadata/android/ru_RU/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/ru_RU/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/sk/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/sk/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/sk/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/uk/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/uk/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/uk/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/vi/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/vi/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/vi/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/full_description.txt b/fastlane/metadata/android/zh_CN/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/zh_CN/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/short_description.txt b/fastlane/metadata/android/zh_CN/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/zh_CN/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/title.txt b/fastlane/metadata/android/zh_CN/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/zh_CN/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/full_description.txt b/fastlane/metadata/android/zh_TW/full_description.txt deleted file mode 100644 index 6863b48341..0000000000 --- a/fastlane/metadata/android/zh_TW/full_description.txt +++ /dev/null @@ -1,55 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - - -Security and accessibility services ---------------------------- - -This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. - -By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. - -It will NOT collect any user data or connect to the internet to send any data anywhere. - -Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/short_description.txt b/fastlane/metadata/android/zh_TW/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/zh_TW/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/title.txt b/fastlane/metadata/android/zh_TW/title.txt deleted file mode 100644 index 9810cafe1e..0000000000 --- a/fastlane/metadata/android/zh_TW/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper & Floating Buttons \ No newline at end of file