diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 5b20b9423b..9173a48fde 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -10,6 +10,25 @@ concurrency: cancel-in-progress: true jobs: + test: + name: Run unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'oracle' + java-version: 17 + cache: 'gradle' + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Unit tests + run: bash ./gradlew testDebugUnitTest + apk: name: Build and release to production runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c5c1c00520..8c1ab5cfbd 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,6 +4,25 @@ on: pull_request: jobs: + test: + name: Run unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'oracle' + java-version: 17 + cache: 'gradle' + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Unit tests + run: bash ./gradlew testDebugUnitTest + style: name: Code style check runs-on: ubuntu-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8696db012f..6e707b4fbf 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -10,6 +10,25 @@ concurrency: cancel-in-progress: true jobs: + test: + name: Run unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'oracle' + java-version: 17 + cache: 'gradle' + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + + - name: Unit tests + run: bash ./gradlew testDebugUnitTest + style: name: Code style check runs-on: ubuntu-latest diff --git a/app/build.gradle b/app/build.gradle index 768ddcb447..a91d792b60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,8 +131,8 @@ dependencies { compileOnly project(":systemstubs") def room_version = "2.6.1" - def coroutinesVersion = "1.5.0" - def nav_version = "2.7.7" + def coroutinesVersion = "1.9.0" + def nav_version = '2.7.7' def work_version = "2.9.1" def epoxy_version = "4.6.2" def splitties_version = "3.0.0" 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 2bfb975d49..b136dec7b2 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 @@ -55,18 +55,7 @@ class ConstraintSnapshotImpl( } } - override fun isSatisfied(constraintState: ConstraintState): Boolean = - when (constraintState.mode) { - ConstraintMode.AND -> { - constraintState.constraints.all { isSatisfied(it) } - } - - ConstraintMode.OR -> { - constraintState.constraints.any { isSatisfied(it) } - } - } - - private fun isSatisfied(constraint: Constraint): Boolean { + override fun isSatisfied(constraint: Constraint): Boolean { val isSatisfied = when (constraint) { is Constraint.AppInForeground -> appInForeground == constraint.packageName is Constraint.AppNotInForeground -> appInForeground != constraint.packageName @@ -98,7 +87,6 @@ class ConstraintSnapshotImpl( is Constraint.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.lens) is Constraint.FlashlightOn -> cameraAdapter.isFlashlightOn(constraint.lens) is Constraint.WifiConnected -> { - Timber.d("Connected WiFi ssid = $connectedWifiSSID") if (constraint.ssid == null) { // connected to any network connectedWifiSSID != null @@ -139,5 +127,22 @@ class ConstraintSnapshotImpl( } interface ConstraintSnapshot { - fun isSatisfied(constraintState: ConstraintState): Boolean + fun isSatisfied(constraint: Constraint): Boolean +} + +fun ConstraintSnapshot.isSatisfied(constraintState: ConstraintState): Boolean { + // Required in case OR is used with empty list of constraints. + if (constraintState.constraints.isEmpty()) { + return true + } + + return when (constraintState.mode) { + ConstraintMode.AND -> { + constraintState.constraints.all { isSatisfied(it) } + } + + ConstraintMode.OR -> { + constraintState.constraints.any { isSatisfied(it) } + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt index 7753ee2a19..212149d0f3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/SimpleMappingController.kt @@ -4,6 +4,7 @@ import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.actions.RepeatMode import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.constraints.isSatisfied import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.util.InputEventType import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt index 91e73c651f..2e5a83a7de 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/KeyMapController.kt @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.actions.PerformActionsUseCase import io.github.sds100.keymapper.constraints.ConstraintSnapshot import io.github.sds100.keymapper.constraints.ConstraintState import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.constraints.isSatisfied import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.entities.ActionEntity import io.github.sds100.keymapper.mappings.ClickType @@ -324,7 +325,7 @@ class KeyMapController( } } - parallelTriggers.forEach { triggerIndex -> + for (triggerIndex in parallelTriggers) { val trigger = triggers[triggerIndex] trigger.keys.forEachIndexed { keyIndex, key -> @@ -385,13 +386,13 @@ class KeyMapController( /** * All sequence events that have the long press click type. */ - private var longPressSequenceTriggerKeys: Array = arrayOf() + private var longPressSequenceTriggerKeys: Array = arrayOf() /** * All double press keys and the index of their corresponding trigger. first is the event and second is * the trigger index. */ - private var doublePressTriggerKeys: Array = arrayOf() + private var doublePressTriggerKeys: Array = arrayOf() /** * order matches with [doublePressTriggerKeys] @@ -405,7 +406,7 @@ class KeyMapController( */ private var doublePressTimeoutTimes = longArrayOf() - private var actionMap = SparseArrayCompat() + private var actionMap: SparseArrayCompat = SparseArrayCompat() private var triggers: Array = emptyArray() /** @@ -424,9 +425,9 @@ class KeyMapController( /** * The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers] */ - private var sequenceTriggersOverlappingSequenceTriggers = arrayOf() + private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf() - private var sequenceTriggersOverlappingParallelTriggers = arrayOf() + private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf() /** * An array of the index of the last matched event in each trigger. @@ -436,7 +437,7 @@ class KeyMapController( /** * An array of the constraints for every trigger */ - private var triggerConstraints: Array = arrayOf() + private var triggerConstraints: Array = arrayOf() /** * The events to detect for each parallel trigger. @@ -447,7 +448,7 @@ class KeyMapController( * The actions to perform when each trigger is detected. The order matches with * [triggers]. */ - private var triggerActions: Array = arrayOf() + private var triggerActions: Array = arrayOf() /** * Stores whether each event in each parallel trigger need to be released after being held down. @@ -612,38 +613,59 @@ class KeyMapController( val constraintSnapshot: ConstraintSnapshot by lazy { detectConstraints.getSnapshot() } + /** + * Store which triggers are currently satisfied by the constraints. + * This is used to check later on whether to wait for a double press to complete + * before executing a short press. See issue #1271. + */ + val triggersSatisfiedByConstraints = mutableSetOf() + + for (triggerIndex in parallelTriggers.plus(sequenceTriggers)) { + val constraintState = triggerConstraints[triggerIndex] + + if (constraintSnapshot.isSatisfied(constraintState)) { + triggersSatisfiedByConstraints.add(triggerIndex) + } + } + // consume sequence trigger keys until their timeout has been reached - for (sequenceTriggerIndex in sequenceTriggers) { - val timeoutTime = sequenceTriggersTimeoutTimes[sequenceTriggerIndex] ?: -1 - val trigger = triggers[sequenceTriggerIndex] - val constraintState = triggerConstraints[sequenceTriggerIndex] + for (triggerIndex in sequenceTriggers) { + val timeoutTime = sequenceTriggersTimeoutTimes[triggerIndex] ?: -1 - if (constraintState.constraints.isNotEmpty()) { - if (!constraintSnapshot.isSatisfied(constraintState)) continue + if (!triggersSatisfiedByConstraints.contains(triggerIndex)) { + continue } if (timeoutTime != -1L && currentTime >= timeoutTime) { - lastMatchedEventIndices[sequenceTriggerIndex] = -1 - sequenceTriggersTimeoutTimes[sequenceTriggerIndex] = -1 + lastMatchedEventIndices[triggerIndex] = -1 + sequenceTriggersTimeoutTimes[triggerIndex] = -1 } else { + val triggerKeys = triggers[triggerIndex].keys + // consume the event if the trigger contains this keycode. - trigger.keys.forEachIndexed { keyIndex, key -> - if (key.keyCode == event.keyCode && trigger.keys[keyIndex].consumeKeyEvent) { + for ((keyIndex, key) in triggerKeys.withIndex()) { + if (key.keyCode == event.keyCode && triggerKeys[keyIndex].consumeKeyEvent) { consumeEvent = true } } } } - doublePressTimeoutTimes.forEachIndexed { doublePressEventIndex, timeoutTime -> + for ((doublePressEventIndex, timeoutTime) in doublePressTimeoutTimes.withIndex()) { if (currentTime >= timeoutTime) { doublePressTimeoutTimes[doublePressEventIndex] = -1 doublePressEventStates[doublePressEventIndex] = NOT_PRESSED } else { val eventLocation = doublePressTriggerKeys[doublePressEventIndex] + val triggerIndex = eventLocation.triggerIndex + + // Ignore this double press trigger if the constraint isn't satisfied. + if (!triggersSatisfiedByConstraints.contains(triggerIndex)) { + continue + } + val doublePressEvent = triggers[eventLocation.triggerIndex].keys[eventLocation.keyIndex] - val triggerIndex = eventLocation.triggerIndex triggers[triggerIndex].keys.forEachIndexed { eventIndex, event -> if (event == doublePressEvent && @@ -669,16 +691,13 @@ class KeyMapController( Otherwise the order of the key maps affects the logic. */ triggerLoop@ for (triggerIndex in parallelTriggers) { - val trigger = triggers[triggerIndex] - val lastMatchedIndex = lastMatchedEventIndices[triggerIndex] + if (!triggersSatisfiedByConstraints.contains(triggerIndex)) { + continue + } - val constraintState = triggerConstraints[triggerIndex] + val trigger = triggers[triggerIndex] - if (constraintState.constraints.isNotEmpty()) { - if (!constraintSnapshot.isSatisfied(constraintState)) { - continue - } - } + val lastMatchedIndex = lastMatchedEventIndices[triggerIndex] for (actionKey in triggerActions[triggerIndex]) { if (canActionBePerformed[actionKey] == null) { @@ -701,26 +720,22 @@ class KeyMapController( val nextIndex = lastMatchedIndex + 1 - if (trigger.matchingEventAtIndex( - event.withShortPress, - nextIndex, - ) - ) { + if (trigger.matchingEventAtIndex(event.withShortPress, nextIndex)) { lastMatchedEventIndices[triggerIndex] = nextIndex parallelTriggerEventsAwaitingRelease[triggerIndex][nextIndex] = true } - if (trigger.matchingEventAtIndex( - event.withLongPress, - nextIndex, - ) - ) { + if (trigger.matchingEventAtIndex(event.withLongPress, nextIndex)) { lastMatchedEventIndices[triggerIndex] = nextIndex parallelTriggerEventsAwaitingRelease[triggerIndex][nextIndex] = true } } triggerLoop@ for (triggerIndex in parallelTriggers) { + if (!triggersSatisfiedByConstraints.contains(triggerIndex)) { + continue + } + val trigger = triggers[triggerIndex] val lastMatchedIndex = lastMatchedEventIndices[triggerIndex] @@ -741,11 +756,7 @@ class KeyMapController( } // Perform short press action - if (trigger.matchingEventAtIndex( - event.withShortPress, - lastMatchedIndex, - ) - ) { + if (trigger.matchingEventAtIndex(event.withShortPress, lastMatchedIndex)) { if (trigger.keys[lastMatchedIndex].consumeKeyEvent) { consumeEvent = true } @@ -773,10 +784,7 @@ class KeyMapController( detectedShortPressTriggers.add(triggerIndex) val vibrateDuration = when { - trigger.vibrate -> { - vibrateDuration(trigger) - } - + trigger.vibrate -> vibrateDuration(trigger) forceVibrate.value -> defaultVibrateDuration.value else -> -1L } @@ -787,11 +795,7 @@ class KeyMapController( } // Perform long press action - if (trigger.matchingEventAtIndex( - event.withLongPress, - lastMatchedIndex, - ) - ) { + if (trigger.matchingEventAtIndex(event.withLongPress, lastMatchedIndex)) { if (trigger.keys[lastMatchedIndex].consumeKeyEvent) { consumeEvent = true } @@ -799,8 +803,7 @@ class KeyMapController( if (lastMatchedIndex == trigger.keys.lastIndex) { awaitingLongPress = true - if (trigger.longPressDoubleVibration - ) { + if (trigger.longPressDoubleVibration) { useCase.vibrate(vibrateDuration(trigger)) } @@ -836,8 +839,16 @@ class KeyMapController( } if (detectedShortPressTriggers.isNotEmpty()) { - val matchingDoublePressEvent = doublePressTriggerKeys.any { - triggers[it.triggerIndex].keys[it.keyIndex].matchesEvent(event.withDoublePress) + val matchingDoublePressEvent = doublePressTriggerKeys.any { keyLocation -> + // See issue #1271. Only consider the double press triggers that overlap + // if the constraints allow it. + + if (!triggersSatisfiedByConstraints.contains(keyLocation.triggerIndex)) { + return@any false + } + + val key = triggers[keyLocation.triggerIndex].keys[keyLocation.keyIndex] + key.matchesEvent(event.withDoublePress) } /* to prevent the actions of keys mapped to a short press and, a long press or a double press @@ -852,16 +863,17 @@ class KeyMapController( performActionsOnFailedLongPress.addAll(detectedShortPressTriggers) } - else -> detectedShortPressTriggers.forEach { triggerIndex -> + else -> { + for (triggerIndex in detectedShortPressTriggers) { + if (triggers[triggerIndex].showToast) { + showToast = true + } - if (triggers[triggerIndex].showToast) { - showToast = true + parallelTriggerActionPerformers[triggerIndex]?.onTriggered( + calledOnTriggerRelease = false, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + ) } - - parallelTriggerActionPerformers[triggerIndex]?.onTriggered( - calledOnTriggerRelease = false, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - ) } } } @@ -882,13 +894,14 @@ class KeyMapController( return true } - sequenceTriggers.forEach { triggerIndex -> - val trigger = triggers[triggerIndex] - val constraints = triggerConstraints[triggerIndex] + for (triggerIndex in sequenceTriggers) { + if (!triggersSatisfiedByConstraints.contains(triggerIndex)) { + continue + } - if (!constraintSnapshot.isSatisfied(constraints)) return@forEach + val trigger = triggers[triggerIndex] - trigger.keys.forEachIndexed { keyIndex, key -> + for (key in trigger.keys) { val matchingEvent = when { key.matchesEvent(event.withShortPress) -> true key.matchesEvent(event.withLongPress) -> true @@ -1015,7 +1028,7 @@ class KeyMapController( // the index of the next event to match in the trigger val nextIndex = lastMatchedEventIndex + 1 - if ((currentTime - downTime) >= longPressDelay(triggers[triggerIndex])) { + if ((currentTime - downTime) >= longPressDelay(trigger)) { successfulLongPressTrigger = true } else if (detectSequenceLongPresses && longPressSequenceTriggerKeys.any { it.matchesEvent(event.withLongPress) } @@ -1036,12 +1049,8 @@ class KeyMapController( } // if the next event matches the event just pressed - if (triggers[triggerIndex].matchingEventAtIndex( - encodedEventWithClickType, - nextIndex, - ) - ) { - if (triggers[triggerIndex].keys[nextIndex].consumeKeyEvent) { + if (trigger.matchingEventAtIndex(encodedEventWithClickType, nextIndex)) { + if (trigger.keys[nextIndex].consumeKeyEvent) { consumeEvent = true } @@ -1053,7 +1062,7 @@ class KeyMapController( */ if (nextIndex == 0) { val startTime = currentTime - val timeout = sequenceTriggerTimeout(triggers[triggerIndex]) + val timeout = sequenceTriggerTimeout(trigger) sequenceTriggersTimeoutTimes[triggerIndex] = startTime + timeout } @@ -1062,16 +1071,16 @@ class KeyMapController( If the last event in a trigger has been matched, then the action needs to be performed and the timer reset. */ - if (nextIndex == triggers[triggerIndex].keys.lastIndex) { + if (nextIndex == trigger.keys.lastIndex) { detectedSequenceTriggerIndexes.add(triggerIndex) - if (triggers[triggerIndex].showToast) { + if (trigger.showToast) { showToast = true } - triggerActions[triggerIndex].forEachIndexed { index, _ -> - if (triggers[triggerIndex].vibrate) { - vibrateDurations.add(vibrateDuration(triggers[triggerIndex])) + triggerActions[triggerIndex].forEach { _ -> + if (trigger.vibrate) { + vibrateDurations.add(vibrateDuration(trigger)) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt index 486dbf0a45..db86d7bb52 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidAppShortcutAdapter.kt @@ -110,8 +110,12 @@ class AndroidAppShortcutAdapter(context: Context) : AppShortcutAdapter { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { - val pendingIntent = - PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) + // See issue #1222 and #1307. Must have FLAG_UPDATE_CURRENT so that + // the intent data is updated. If you don't do this and have two app shortcut actions + // from the same app then the data isn't updated and both actions will send + // the pending intent for the shortcut that was triggered first. + val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + val pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags) pendingIntent.send() return Success(Unit) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt index fbb8fcbf04..815b868bb8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationController.kt @@ -108,8 +108,8 @@ class NotificationController( /** * Open the app and use the String as the Intent action. */ - private val _openApp: MutableSharedFlow = MutableSharedFlow() - val openApp: SharedFlow = _openApp.asSharedFlow() + private val _openApp: MutableSharedFlow = MutableSharedFlow() + val openApp: SharedFlow = _openApp.asSharedFlow() private val _showToast = MutableSharedFlow() val showToast = _showToast.asSharedFlow() @@ -213,7 +213,7 @@ class NotificationController( ACTION_STOP_SERVICE -> controlAccessibilityService.stopService() ACTION_DISMISS_TOGGLE_MAPPINGS -> manageNotifications.dismiss(ID_TOGGLE_MAPPINGS) - ACTION_OPEN_KEY_MAPPER -> _openApp.emit(null) + ACTION_OPEN_KEY_MAPPER -> _openApp.emit("") ACTION_SHOW_IME_PICKER -> showImePicker.show(fromForeground = false) ACTION_SHOW_KEYBOARD -> hideInputMethod.show() ACTION_TOGGLE_KEYBOARD -> toggleCompatibleIme.toggle().onSuccess { @@ -224,12 +224,12 @@ class NotificationController( ACTION_FINGERPRINT_GESTURE_FEATURE -> { onboardingUseCase.approvedFingerprintFeaturePrompt = true - _openApp.emit(null) + _openApp.emit("") } ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN -> { onboardingUseCase.approvedSetupChosenDevicesAgainNotification() - _openApp.emit(null) + _openApp.emit("") } } }.flowOn(dispatchers.default()).launchIn(coroutineScope) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..8b2b1737b0 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..094f2853d8 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,1055 @@ + + + Giải phóng chìa khóa của bạn! + Không có hành động nào được chọn + Key Mapper yêu cầu sử dụng dịch vụ trợ năng để dịch vụ này có thể phát hiện và thay đổi thao tác nhấn nút của bạn khi bạn ở ngoài ứng dụng. Bản đồ chính của bạn sẽ chỉ hoạt động khi bạn đã bật dịch vụ trợ năng. Nó cũng phải được bật để tạo trình kích hoạt. + %d Đã chọn + Cho phép + Mở + ¯\\_(ツ)_/¯\n\nKhông có gì ở đây! + Ghi thao tác kích hoạt! + Thêm hành động! + ¯\\_(ツ)_/¯\n\n Tạo một hành động! + ¯\\_(ツ)_/¯\n\nBạn chưa chọn bất kỳ hành động nào cho phím tắt này! + Bạn chưa tạo bất kỳ bản đồ quan trọng nào! + Mọi thứ đều có vẻ tốt! + Ứng dụng sẽ hoạt động nhưng có thể cần khắc phục một số lỗi tùy thuộc vào việc bạn đang làm. + Thiết bị của bạn không hỗ trợ một số hành động. + Hành động không được hỗ trợ + Yêu cầu root + Nhấn… + Không có hành động nào + Không có hành động kích hoạt + Mã khóa không xác định: %s + Tên thiết bị không xác định + Bật + Tắt + Theo dõi hệ thống + Thiết bị này + Bất kỳ thiết bị nào + Không biết tên của thiết bị này + Mặc định + Bật dịch vụ trợ năng + Khởi động lại dịch vụ trợ năng + Chia sẻ + Dừng lặp lại khi… + Trình kích hoạt được phát hiện + Phím được nhấn lại + Đã đạt đến giới hạn + Vuốt lại + Trình kích hoạt được phát hiện + Phím được nhấn lại + Hiển thị ứng dụng ẩn + Sửa đổi + Chuyển đổi + QUAN TRỌNG!!! Các tọa độ này chỉ chính xác khi màn hình của bạn cùng hướng với ảnh chụp màn hình! Hành động này sẽ hủy mọi thao tác chạm hoặc cử chỉ bạn đang thực hiện trên màn hình.\n\nNếu bạn cần trợ giúp tìm tọa độ của một điểm trên màn hình, hãy chụp ảnh màn hình rồi nhấn vào ảnh chụp màn hình nơi bạn muốn nhấn vào hành động này. + Lưu ý: Khi sử dụng \"pinch in\" X và Y là tọa độ END, khi sử dụng \"pinch out\" X và Y là tọa độ START. + Không biết tên máy! + Nhấn vào hành động để khắc phục! + Nhấn vào các hạn chế để khắc phục! + Thực hiện hành động + Giữ cho đến khi kích hoạt… + Không có thiết bị + ¯\\_(ツ)_/¯\n\nKhông có tính năng bổ sung! + Tạo sơ đồ phím mới + Đã hoàn tất việc định cấu hình hành động sự kiện quan trọng + Chọn tọa độ xong + Nhật ký hành động + Gửi tới + Có gì mới + Bấm vào phím trên thiết bị mà bạn muốn nhập. \n\nQUAN Trọng! Việc nhập phím này dưới dạng hành động sẽ chỉ hoạt động nếu bạn đang sử dụng bàn phím tương thích với Key Mapper. + QUAN TRỌNG! Việc nhập mã khóa này dưới dạng hành động sẽ chỉ có tác dụng nếu bạn đang sử dụng bàn phím tương thích với Key Mapper. + Tệp âm thanh sẽ được sao chép vào thư mục dữ liệu riêng tư của Key Mapper, điều đó có nghĩa là các thao tác của bạn vẫn hoạt động ngay cả khi tệp bị di chuyển hoặc xóa. Nó cũng sẽ được sao lưu cùng với bản đồ chính của bạn trong thư mục zip. Bạn có thể xóa các tập tin âm thanh đã lưu trong cài đặt. + Nhập một số văn bản bạn muốn chèn vào khi thực hiện hành động này. + Nhập số điện thoại. + Nhập URL mà bạn muốn mở. http://, https:// hoặc www. không bắt buộc. + Không thể tìm thấy bất kỳ thiết bị được ghép nối nào. Bluetooth đã được bật chưa? + Tùy chọn \"Cho phép các ứng dụng khác kích hoạt sơ đồ bàn phím này\" sẽ được bật cho sơ đồ bàn phím mà bạn chọn nếu chưa bật. Nếu sau này bạn tắt tùy chọn này thì mọi phím tắt hoặc Ý định kích hoạt sơ đồ phím này sẽ không hoạt động. + Đã bật + Tàn tật + Khôi phục + Sau khi đã bật quản trị viên thiết bị, bạn phải TẮT KÍCH HOẠT nó nếu muốn gỡ cài đặt Key Mapper. + Thêm một hạn chế! + Đợi %sms + Bắt đầu hoạt động: %s + Bắt đầu dịch vụ: %s + Gửi phát sóng: %s + Sơ đồ chính UUID + Sử dụng shell (chỉ ROOT) + Key Mapper yêu cầu quyền sửa đổi chế độ Không làm phiền nếu bạn muốn các nút hoạt động như mong đợi ở chế độ Không làm phiền! + Trình kích hoạt này sẽ không hoạt động như mong đợi ở chế độ Không làm phiền! + Tùy chọn kích hoạt khi màn hình tắt cần có quyền root để hoạt động! + Tùy chọn kích hoạt khi màn hình tắt sẽ không hoạt động! + Trình kích hoạt này sẽ không hoạt động khi đang đổ chuông hoặc đang gọi điện thoại! + Android không cho phép các dịch vụ trợ năng phát hiện các lần nhấn nút âm lượng khi điện thoại của bạn đang đổ chuông hoặc đang trong cuộc gọi điện thoại, nhưng nó cho phép các dịch vụ phương thức nhập liệu phát hiện chúng. Do đó, bạn phải sử dụng một trong các bàn phím Key Mapper nếu muốn trình kích hoạt này hoạt động. + Quá nhiều ngón tay để thực hiện cử chỉ do giới hạn của Android. + Thời lượng cử chỉ quá cao do giới hạn của Android. + Hành động của bạn sẽ ngừng hoạt động ngẫu nhiên! + Hành động của bạn đã bị tạm dừng! + Bỏ tạm dừng + Dịch vụ trợ năng cần được bật để hành động của bạn hoạt động! + Điện thoại của bạn đã tắt Key Mapper khi nó ở chế độ nền hoặc bị hỏng! + Dịch vụ trợ năng đã được kích hoạt! Hành động của bạn sẽ hoạt động. + Ghi nhật ký bổ sung được bật! Hãy tắt tính năng này nếu bạn không cố gắng khắc phục sự cố. + Tắt + Về + + Mở %s + Nhấn phím \'%s\' + Nhập \'%s\' + Nhập %s%s + Nhập %s qua shell + Nhập %s%s từ %s + Mở %s + Nhấn vào tọa độ %d, %d + Nhấn vào tọa độ %d, %d (%s) + Vuốt bằng %d ngón tay từ tọa độ %d/%d đến %d/%d trong %dms + Vuốt bằng %d ngón tay từ tọa độ %d/%d đến %d/%d trong %dms (%s) + %s bằng %d ngón tay(s) trên tọa độ %d/%d với khoảng cách chụm là %dpx tính bằng %dms + %s với %d ngón tay trên tọa độ %d/%d đến với khoảng cách chụm là %dpx %dms (%s) + Gọi %s + Phát âm thanh: %s + + + Tùy chọn + Hành động + Trigger + Hạn chế + Vuốt lên + Vuốt xuống + Vuốt sang trái + Vuốt sang phải + Kiểu nhấp chuột + Tiện ích bổ sung + + + Bắt đầu X + Bắt đầu Y + Kết thúc X + Kết thúc Y + Khoảng cách chụm (px) + Kiểu kẹp + Chụm vào + Chụm ra + Mã khóa + Từ thiết bị + Tên phím tắt + Mô tả tọa độ (tùy chọn) + Văn bản cần nhập + Url để mở + Số điện thoại để gọi + Hoạt động + Thể loại + Dữ liệu + Bưu kiện + Class + Tên + Giá trị (%s) + Mô tả cho Key Mapper (bắt buộc) + Flags + Mô tả tập tin âm thanh + SSID mạng Wi-Fi + + + Đồng thời + Theo thứ tự + + HOẶC + Bấm nhanh + Nhấn giữ + Nhấn đúp + Đúng + Sai + Hoạt động + Dịch vụ + Máy thu phát sóng + + + Kích hoạt và hành động + Những hạn chế và hơn thế nữa + Tùy chọn + Hạn chế + Hành động + Trigger + Dấu vân tay + + @string/tab_keyevents + @string/tab_fingerprint + + + + + + Đã chọn %s + Sao lưu thành công! + Sao lưu thất bại! + Khôi phục thành công! + Khôi phục thất bại! + Tự động sao lưu thành công! + Tự động sao lưu thất bại! + Đã chụp ảnh màn hình + IO exception ¯\\_(ツ)_/¯ + Độ phân giải ảnh chụp màn hình không khớp với độ phân giải của thiết bị này! + Đã sao chép sơ đồ khóa UUID vào bảng nhớ tạm + Bạn đã kích hoạt hành động + Nhật ký được sao chép + Tính năng root hiện đã được kích hoạt + Chọn tập tin âm thanh không thành công! Kiểm tra nhật ký. + Bạn chưa lưu tập tin âm thanh nào! + Key Mapper đã sử dụng Shizuku để tự cấp quyền WRITE_SECURE_SETTINGS + Key Mapper đã sử dụng Root để tự cấp quyền WRITE_SECURE_SETTINGS + + + Sequence trigger timeout (ms) + Độ trễ nhấn giữ (ms) + Thời gian chờ nhấn đúp (ms) + Trì hoãn cho đến khi lặp lại (ms) + Giới hạn lặp lại + Lặp lại mỗi… (ms) + Thời gian rung (ms) + Đã bao nhiêu lần + Mỗi lần lặp lại bao nhiêu lần + + Thời gian giữ phím (ms) + Thời lượng vuốt (ms) + Đếm ngón tay + Tọa độ để thiết lập với ảnh chụp màn hình + Bắt đầu + Kết thúc + Thời lượng chụm (ms) + Đếm ngón tay + + + %s ở phía trước + %s không ở phía trước + %s đang phát phương tiện + %s không phát phương tiện + %s đã được kết nối + %s bị ngắt kết nối + Màn hình bật + Màn hình tắt + đèn pin %s đang tắt + Đèn pin %s đang bật + + Hoặc + Ứng dụng + Bluetooth + Màn hình + Định hướng + Ứng dụng ở phía trước + Ứng dụng không ở nền trước + Thiết bị Bluetooth đã được kết nối + Thiết bị Bluetooth bị ngắt kết nối + Màn hình bật + Màn hình tắt + Màn hình xoay (0°) + Màn hình xoay (90°) + Màn hình xoay (180°) + Màn hình xoay (270°) + Màn hình xoay (bất kỳ) + Phong cảnh (bất kỳ) + Ứng dụng phát âm thanh + Ứng dụng không phát âm thanh + Âm thanh đang phát + Không có âm thanh nào đang phát + Đèn pin đang bật + Đèn pin đã tắt + WiFi đang bật + Wi-Fi đã tắt + Đã kết nối với mạng WiFi + Đã ngắt kết nối khỏi mạng WiFi + Bạn sẽ phải nhập SSID theo cách thủ công vì các ứng dụng không được phép truy vấn danh sách các mạng WiFi đã biết trên Android 10 trở lên. Để trống nếu có bất kỳ mạng WiFi nào phù hợp. + Bất kì + Đã kết nối với WiFi %s + Đã ngắt kết nối với WiFi %s + Đã kết nối với bất kỳ WiFi nào + Đã ngắt kết nối và không có WiFi + Phương thức nhập được chọn + %s được chọn + Phương thức nhập không được chọn + %s không được chọn + Thiết bị đã bị khóa + Thiết bị đã được mở khóa + Trong cuộc gọi điện thoại + Không có trong cuộc gọi điện thoại + Điện thoại đổ chuông + Sạc + Xả + Màn hình xoay (0°) + Màn hình xoay (90°) + Màn hình xoay (180°) + Màn hình xoay (270°) + + + Bấm nhanh + Nhấn giữ + Nhấn đúp + + + + + Chọn hành động + + + Tắt tính năng diệt ứng dụng + Hãy làm theo hướng dẫn tại Dontkillmyapp.com để chỉ cho bạn cách tắt tất cả các \"tính năng\" tiêu diệt ứng dụng trên điện thoại của bạn. \n\nSau khi đọc hướng dẫn, bạn sẽ cần chuyển sang trang trình bày tiếp theo và khởi động lại dịch vụ trợ năng. + Mở hướng dẫn + Khởi động lại dịch vụ trợ năng + Dịch vụ trợ năng phải được khởi động lại. Tắt nó đi và bật lại. + Khởi động lại + Báo cáo lỗi + Chọn vị trí để lưu báo cáo lỗi bằng cách nhấn vào \"tạo báo cáo\". Trang trình bày tiếp theo sẽ cho biết cách bạn có thể gửi nó cho nhà phát triển. + Tạo báo cáo + Chia sẻ báo cáo + Có 2 cách để chia sẻ báo cáo lỗi cho nhà phát triển. Tham gia máy chủ Discord hoặc tạo sự cố GitHub. Đảm bảo rằng bạn đính kèm báo cáo lỗi vào tin nhắn của mình! + Discord + GitHub + + + Cài đặt + Xong + Chọn tất cả + Về + Tìm kiếm + Hướng dẫn bắt đầu nhanh + Help + Cho phép + Vô hiệu hóa + Vô hiệu hóa tất cả + Kích hoạt tất cả + Báo cáo lỗi + Hiển thị hộp chọn phương thức nhập + Cơ sở dữ liệu hạt giống + Lưu + Nhân bản + Lên + Khôi phục + Sao lưu mọi thứ + Sao lưu tất cả + Nhấn để tạm dừng + Nhấn để tiếp tục + Khôi phục + Lưu + Chuyển đổi tin nhắn ngắn + Sao chép + Xóa + + + Thêm hành động + Ghi kích hoạt + Xong + Lưu + Sửa chữa + %d… + Thêm hạm chế + Chọn mã khóa + Đúng! + Chọn hành động + Thêm bổ sung + Tạo lối tắt trình khởi chạy + Tạo lối tắt thủ công + Hướng dẫn ý định + Help + Chọn ảnh chụp màn hình (tùy chọn) + Chọn hoạt động + Set flags + Sao chép + Không giới hạn + Chọn tập tin âm thanh + Chỉnh sửa hành động + Thay thế hành động + + + Bạn có chắc không? + Tạo tiêu đề lối tắt + Cần có quyền root! + Hơn + Chọn luồng + Chọn đèn nháy + Không thể tìm thấy trang cài đặt trợ năng + Không thể ghi kích hoạt? + Hủy thay đổi của bạn + Bạn có chắc chắn muốn hủy các thay đổi của mình không? + Nếu bạn biết điện thoại của mình chưa được root hoặc bạn không biết root là gì thì bạn không thể sử dụng các tính năng chỉ hoạt động trên các thiết bị đã root. Khi bạn nhấn \'OK\', bạn sẽ được đưa đến cài đặt. Trong cài đặt, cuộn xuống cuối và nhấn \'Key Mapper có quyền root\' để bạn có thể sử dụng các tính năng/hành động gốc. + Đang tải xuống… + Nhấn lâu chỉ có tác dụng với các nút âm lượng và điều hướng vật lý. Nếu bạn bật tính năng này cho các phím khác, các phím đó sẽ không hoạt động khi chúng không được nhấn lâu. + Cấp quyền WRITE_SECURE_SETTINGS + Cần có PC/Mac để cấp quyền này. Đọc hướng dẫn trực tuyến. + Thiết bị của bạn dường như không có trang cài đặt dịch vụ trợ năng. Nhấn vào \"hướng dẫn\" để đọc hướng dẫn trực tuyến giải thích cách khắc phục vấn đề này. + "Nhiều phím không thể được nhấn đúp cùng một lúc." + Các phím cần được liệt kê từ trên xuống dưới theo thứ tự chúng sẽ được giữ. + Trình kích hoạt \"trình tự\" có thời gian chờ không giống như trình kích hoạt song song. Điều này có nghĩa là sau khi nhấn phím đầu tiên, bạn sẽ có một khoảng thời gian nhất định để nhập các phím còn lại vào bộ kích hoạt. Tất cả các phím mà bạn đã thêm vào trình kích hoạt sẽ không thực hiện hành động thông thường cho đến khi hết thời gian chờ. Bạn có thể thay đổi thời gian chờ này trong tab \"Tùy chọn\". + Android không cho phép ứng dụng nhận danh sách các thiết bị Bluetooth được kết nối (không ghép nối). Ứng dụng chỉ có thể phát hiện khi chúng được kết nối và ngắt kết nối. Vì vậy, nếu thiết bị Bluetooth của bạn đã được kết nối với thiết bị của bạn khi dịch vụ trợ năng khởi động, bạn sẽ phải kết nối lại thiết bị đó để ứng dụng biết thiết bị đó đã được kết nối. + Thay đổi vị trí sao lưu hoặc tắt tự động sao lưu? + Các ràng buộc bật/tắt màn hình sẽ chỉ hoạt động nếu bạn đã bật tùy chọn sơ đồ phím \"phát hiện trình kích hoạt khi màn hình tắt\". Tùy chọn này sẽ chỉ hiển thị đối với một số phím (ví dụ: nút âm lượng) và nếu bạn đã root. Xem danh sách các phím được hỗ trợ trên trang Trợ giúp. + Nếu bạn đã chọn bất kỳ khóa màn hình nào khác, chẳng hạn như mã PIN hoặc Mẫu thì bạn không phải lo lắng. Nhưng nếu bạn có khóa màn hình Mật khẩu, bạn sẽ *KHÔNG* có thể mở khóa điện thoại của mình nếu bạn sử dụng Phương thức nhập cơ bản của Key Mapper vì nó không có GUI. Bạn có thể cấp quyền cho Key Mapper WRITE_SECURE_SETTINGS để nó có thể hiển thị thông báo chuyển sang và từ bàn phím. Có hướng dẫn về cách thực hiện việc này nếu bạn nhấn vào dấu chấm hỏi ở cuối màn hình. + Chọn phương thức nhập cho các hành động yêu cầu. Bạn có thể thay đổi điều này sau bằng cách nhấn vào \"Chọn bàn phím để thực hiện thao tác\" ở menu dưới cùng của màn hình chính. + Bạn cần chọn bố cục bàn phím \"Caps Lock to camera\" cho bàn phím của mình nếu không phím Caps Lock vẫn sẽ khóa mũ. Bạn có thể tìm thấy cài đặt này trong cài đặt thiết bị của mình -> Ngôn ngữ và Phương thức nhập -> Bàn phím vật lý -> Nhấn vào bàn phím của bạn -> Thiết lập bố cục bàn phím. Thao tác này sẽ ánh xạ lại phím Caps Lock thành KEYCODE_CAMERA để Key Mapper có thể ánh xạ lại phím đó đúng cách.\n\nSau khi thực hiện xong việc này, bạn phải xóa phím kích hoạt Caps Lock và ghi lại phím Caps Lock. Nó sẽ hiện \"Camera\" thay vì \"Caps Lock\" nếu bạn thực hiện đúng các bước. + Khởi động lại thiết bị của bạn nếu nút \"Kích hoạt ghi\" đang đếm ngược và các nút bạn đang nhấn không hiển thị. Nếu các nút của bạn vẫn không hiển thị sau khi khởi động lại thì Key Mapper không hỗ trợ các nút của bạn. Không có cách khắc phục cho việc này. + Không có thiết bị bên ngoài nào được kết nối. + Cài đặt bàn phím GUI Key Mapper + Điều này rất được khuyến khích! Đây là bàn phím thích hợp mà bạn có thể sử dụng với Key Mapper. Công cụ tích hợp sẵn trong Key Mapper (Phương thức nhập liệu cơ bản) không có bàn phím trên màn hình. Chọn nơi bạn muốn cài đặt nó. + Cài đặt Bàn phím Leanback của Key Mapper + Điều này rất được khuyến khích! Đây là bàn phím thích hợp cho Android TV mà bạn có thể sử dụng với Key Mapper. Công cụ tích hợp sẵn trong Key Mapper (Phương thức nhập liệu cơ bản) không có bàn phím trên màn hình. Chọn nơi bạn muốn cài đặt nó. + Cài đặt bàn phím GUI Key Mapper + Chọn nơi bạn muốn tải xuống từ đó. + Cài đặt Bàn phím Leanback của Key Mapper + Chọn nơi bạn muốn tải xuống từ đó. + Hành động này cần thiết lập thêm + Có 3 cách để bạn có thể thiết lập thiết bị của mình sử dụng tác vụ này. Dưới đây là những ưu điểm và nhược điểm của mỗi. \n\n1. Tải xuống Shizuku (được khuyến nghị). Bạn không cần phải sử dụng bàn phím ảo khác với bàn phím bạn đang sử dụng nhưng sẽ cần một phút thiết lập mỗi khi bạn khởi động lại thiết bị của mình. \n\n2. Tải xuống Bàn phím GUI Key Mapper. Đây là bàn phím ảo mà bạn có thể sử dụng với Key Mapper nhưng bạn sẽ không thể sử dụng bàn phím mà bạn hiện đang sử dụng, chẳng hạn như Gboard. \n\n3. Không làm gì cả và sử dụng bàn phím Key Mapper tích hợp sẵn. Điều này không được khuyến khích vì bạn sẽ không bàn phím ảo khi sử dụng Key Mapper! Không có lợi thế. + Hành động này cần thiết lập thêm + Có 3 cách để bạn có thể thiết lập thiết bị của mình sử dụng tác vụ này. Dưới đây là những ưu điểm và nhược điểm của mỗi. \n\n1. Tải xuống Shizuku (được khuyến nghị). Bạn không cần phải sử dụng bàn phím ảo khác với bàn phím bạn đang sử dụng nhưng sẽ cần một phút thiết lập mỗi khi bạn khởi động lại thiết bị của mình. \n\n2. Tải xuống Bàn phím Leanback Key Mapper. Đây là bàn phím ảo được tối ưu hóa cho Android TV mà bạn có thể sử dụng với Key Mapper nhưng bạn sẽ không thể sử dụng bàn phím mà bạn hiện đang sử dụng, chẳng hạn như Gboard. \n\n3. Không làm gì cả và sử dụng bàn phím Key Mapper tích hợp sẵn. Điều này không được khuyến khích vì bạn sẽ không bàn phím ảo khi sử dụng Key Mapper! Không có lợi thế. + Vô hiệu hóa tối ưu hóa pin + Bạn PHẢI đọc tất cả này nếu không bạn sẽ thất vọng trong tương lai!\n\nNhấn vào \"sửa một phần\" có thể ngăn Android dừng ứng dụng khi ứng dụng đang chạy trong nền.\n\nĐiều này KHÔNG ĐỦ. Giao diện OEM của bạn như MIUI hoặc Samsung Experience có thể có các tính năng tiêu diệt ứng dụng khác, vì vậy bạn PHẢI tắt chúng cho Key Mapper bằng cách làm theo hướng dẫn trực tuyến tại Dontkillmyapp.com. + Gửi phản hồi + Vui lòng đọc hướng dẫn về cách báo cáo sự cố trên trang web. + Bật dịch vụ trợ năng để bạn có thể ghi lại trình kích hoạt. + Khởi động lại dịch vụ trợ năng bằng cách tắtbật để bạn có thể ghi lại trình kích hoạt. + Bật dịch vụ trợ năng để bạn có thể kiểm tra hành động. + Khởi động lại dịch vụ trợ năng bằng cách tắtbật để bạn có thể kiểm tra hành động. + Khởi động lại dịch vụ trợ năng bằng cách tắtbật. + Việc sử dụng trình kích hoạt này có thể gây ra màn hình đen khi bạn mở khóa thiết bị sau khi sử dụng cài đặt ghim màn hình trong cài đặt của thiết bị. Điều này có thể được khắc phục bằng cách khởi động lại. Điều này không xảy ra trên tất cả các thiết bị vì vậy hãy cẩn thận và tắt cài đặt nếu xảy ra! + Đặt lại bản đồ cử chỉ vân tay + Bạn có chắc chắn muốn đặt lại bản đồ cử chỉ vân tay của mình không? + Key Mapper đã bị hỏng + Rất có thể điện thoại của bạn đã tắt Key Mapper khi nó đang cố chạy ở chế độ nền. Đây không là lỗi của nhà phát triển và họ không thể làm gì để khắc phục nên vui lòng đừng để lại đánh giá xấu 😃. + + \n\nTrước đây, bạn đã làm theo hướng dẫn trên Dontkillmyapp.com để ngăn điện thoại của mình tắt Key Mapper chưa? + Đúng + Không + Không tạo được báo cáo lỗi + Yêu cầu sự cho phép của Shizuku + Vì bạn đang sử dụng Shizuku nên bạn nên cấp quyền này vì một số tính năng trong Key Mapper có thể được thực hiện mà bạn không cần phải định cấu hình bất cứ điều gì (Ví dụ: nhập mã khóa mà không cần sử dụng bàn phím Key Mapper). + Sửa lỗi + Cần có sự cho phép + Key Mapper cần có quyền \"thiết bị lân cận\" để có thể lấy danh sách các thiết bị Bluetooth được ghép nối. + Bạn chưa cài đặt ứng dụng tệp nào cho phép bạn tạo tệp cho Key Mapper. Vui lòng cài đặt trình quản lý tập tin. + Bạn chưa cài đặt ứng dụng tệp nào cho phép bạn chọn tệp cho Key Mapper. Vui lòng cài đặt trình quản lý tập tin. + Dịch vụ trợ năng phải được kích hoạt + @string/accessibility_service_explanation + Cấp quyền truy cập Không làm phiền + Bạn sẽ được đưa đến trang cài đặt của thiết bị để quản lý những ứng dụng nào có thể sửa đổi trạng thái Không làm phiền. Tính năng này không xuất hiện trên một số thiết bị vì vậy hãy nhấn vào không hiển thị lại nếu bạn không thấy Key Mapper trong danh sách. + Đúng + Xác nhận + Xong + Chọn tham gia + Hướng dẫn + Hướng dẫn + Hướng dẫn + Kích hoạt tính năng root + Khoản trợ cấp + Tham gia + Thay đổi + Sửa một phần + Được rồi + Bật + Khởi động lại + Không bao giờ hiển thị lại + Mở hướng dẫn trực tuyến + Tắt + Tránh xa + Không + Hủy bỏ + Không hiển thị lại + Hướng dẫn trực tuyến + Cài đặt + Tài liệu + Nhật ký thay đổi + Báo cáo lỗi + Khởi động lại + Đi đến hướng dẫn + Shizuku + Bàn phím GUI của Key Mapper + Bàn phím Leanback của Key Mapper + Không làm gì cả + Sửa chữa + Hủy bỏ + Sửa chữa + + + Bộ chọn bàn phím + Tạm dừng/Tiếp tục ánh xạ + Cảnh báo bàn phím bị ẩn + Chuyển đổi bàn phím Key Mapper + Tính năng mới + Nhấn để thay đổi bàn phím của bạn. + Hộp chọn bàn phím + Đang tải xuống… + Nhấn để mở Key Mapper. + Tạm dừng + Đã tạm dừng + Nhấn để mở Key Mapper. + Bản tóm tắt + Miễn nhiệm + Khởi động lại + Dịch vụ trợ năng bị vô hiệu hóa + Nhấn để bắt đầu dịch vụ trợ năng. + Dịch vụ trợ năng cần khởi động lại! + Dịch vụ trợ năng đã bị hỏng! Điện thoại của bạn có thể đang tích cực tắt nó! Nhấn để khởi động lại dịch vụ trợ năng. + + Dừng dịch vụ + Bàn phím bị ẩn! + Nhấn \'hiển thị bàn phím\' để bắt đầu hiển thị lại bàn phím. + Chuyển đổi bàn phím Key Mapper + Nhấn \'chuyển đổi\' để chuyển sang và từ bàn phím Key Mapper. + Chuyển đổi + Ánh xạ lại cử chỉ vân tay với Key Mapper! + Thiết bị của bạn hỗ trợ ánh xạ lại các thao tác vuốt trên cảm biến vân tay. Nhấn để bắt đầu ánh xạ lại! + Bạn cần thiết lập lại một số cài đặt! + Có vẻ như bạn đang sử dụng cài đặt này để tự động thay đổi bàn phím hoặc hiển thị bộ chọn phương thức nhập khi thiết bị Bluetooth kết nối và ngắt kết nối. Key Mapper hiện cho phép bạn sử dụng bất kỳ thiết bị đầu vào nào chứ không chỉ các thiết bị Bluetooth. Không có cách nào để di chuyển cài đặt cũ theo cách chức năng mới sẽ hoạt động, do đó bạn sẽ phải chọn lại thiết bị trong cài đặt Key Mapper. Nhấn để mở Key Mapper. + + + Độ trễ nhấn giữ mặc định (ms) + Cần nhấn một nút trong bao lâu để được phát hiện là nhấn lâu. Mặc định là 500ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Thời lượng nhấn đúp mặc định (ms) + Một nút phải được nhấn đúp nhanh đến mức nào để được phát hiện là nhấn đúp. Mặc định là 300ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Rung trong bao lâu nếu chế độ rung được bật cho sơ đồ phím. Mặc định là 200ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Thời lượng rung mặc định (ms) + Cần giữ nút kích hoạt trong bao lâu để hành động bắt đầu lặp lại. Mặc định là 400ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Độ trễ mặc định cho đến khi lặp lại (ms) + Độ trễ giữa mỗi lần một hành động được lặp lại. Mặc định là 50ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Độ trễ mặc định giữa các lần lặp lại (ms) + Thời gian cho phép để hoàn thành một trình kích hoạt trình tự. Mặc định là 1000ms. Có thể được ghi đè trong các tùy chọn của bản đồ chính. + Thời gian chờ kích hoạt trình tự mặc định (ms) + Khôi phục + Buộc tất cả các bản đồ chính rung. + Buộc rung + Thông báo bộ chọn bàn phím + Hiển thị thông báo liên tục để cho phép bạn chọn bàn phím. + Tạm dừng/tiếp tục thông báo hành động + Hiển thị thông báo liên tục bắt đầu/tạm dừng hành động của bạn. + Tự động sao lưu cài đặt tới một vị trí được chỉ định + Không có địa điểm nào được chọn. + Chọn thiết bị + Tự động hiển thị hộp chọn bàn phím + Khi thiết bị bạn đã chọn kết nối hoặc ngắt kết nối, bộ chọn bàn phím sẽ tự động hiển thị. Chọn các thiết bị dưới đây. + Tự động thay đổi bàn phím ảo khi một thiết bị (ví dụ: bàn phím) kết nối/ngắt kết nối + Bàn phím Key Mapper được sử dụng lần cuối sẽ được chọn tự động khi thiết bị được chọn được kết nối. Bàn phím thông thường của bạn sẽ được tự động chọn khi thiết bị ngắt kết nối. + Tự động thay đổi bàn phím ảo khi bạn bắt đầu nhập văn bản + Bàn phím không phải Key Mapper được sử dụng lần cuối sẽ tự động được chọn khi bạn cố mở bàn phím. Bàn phím Key Mapper của bạn sẽ được chọn tự động sau khi bạn ngừng sử dụng bàn phím. + Hiển thị thông báo trên màn hình khi tự động thay đổi bàn phím + Key Mapper có quyền root + Bật tính năng này nếu bạn muốn sử dụng các tính năng/hành động chỉ hoạt động trên các thiết bị đã root. Key Mapper phải có quyền root từ ứng dụng quản lý quyền truy cập root của bạn (ví dụ: Magisk, SuperSU) để các tính năng này hoạt động. Chỉ bật tính năng này nếu bạn biết thiết bị của mình đã được root và bạn đã cấp quyền root cho Key Mapper. + Chủ đề tối + Cài đặt thông báo + Chuyển đổi giữa bàn phím Key Mapper và bàn phím mặc định của bạn khi bạn chạm vào thông báo. + Chuyển đổi thông báo bàn phím Key Mapper + Tự động thay đổi bàn phím khi chuyển đổi sơ đồ phím + Tự động chọn bàn phím Key Mapper khi bạn tiếp tục lại sơ đồ chính và chọn bàn phím mặc định khi tạm dừng chúng. + Ẩn cảnh báo màn hình chính + Ẩn cảnh báo ở đầu màn hình chính. + Hiển thị 5 ký tự đầu tiên của id thiết bị cho trình kích hoạt cụ thể của thiết bị + Điều này rất hữu ích để phân biệt giữa các thiết bị có cùng tên. + Sửa bàn phím được đặt thành tiếng Anh Mỹ + Điều này khắc phục những bàn phím không có bố cục bàn phím chính xác khi bật dịch vụ trợ năng. Nhấn để đọc thêm và định cấu hình. + Sửa bàn phím được đặt thành tiếng Anh Mỹ + Có một lỗi trong Android 11 là việc bật dịch vụ trợ năng khiến Android nghĩ rằng tất cả các thiết bị bên ngoài đều là cùng một thiết bị ảo bên trong. Vì nó không thể xác định chính xác các thiết bị này nên nó không biết nên sử dụng bố cục bàn phím nào với chúng nên nó mặc định là tiếng Anh Mỹ ngay cả khi đó là bàn phím tiếng Đức chẳng hạn. Bạn có thể sử dụng Key Mapper để khắc phục sự cố này bằng cách thực hiện theo các bước bên dưới. + 4. Chọn thiết bị + 1. Cài đặt Bàn phím GUI Key Mapper (tùy chọn) + 1. Cài đặt Bàn phím Leanback của Key Mapper (tùy chọn) + 2. Kích hoạt Bàn phím GUI Key Mapper hoặc Phương thức nhập cơ bản của Key Mapper + 2. Kích hoạt Bàn phím Leanback của Key Mapper hoặc Phương thức nhập cơ bản của Key Mapper + 3. Sử dụng bàn phím bạn vừa kích hoạt + (Được khuyến nghị) Đọc hướng dẫn sử dụng cho cài đặt này. + Cho phép ghi nhật ký bổ sung + Xem và chia sẻ nhật ký + Báo cáo vấn đề + Xóa tập tin âm thanh + Xóa các tệp âm thanh có thể được sử dụng cho tác vụ Âm thanh. + Cấp quyền + Đã cấp quyền + 1. Shizuku chưa được cài đặt! Nhấn để tải xuống ứng dụng Shizuku. + 1. Shizuku đã được cài đặt. + 2. Shizuku chưa bắt đầu! Nhấn để mở ứng dụng Shizuku rồi đọc hướng dẫn giải thích cách khởi động ứng dụng. + 2. Shizuku bắt đầu. + 3. Key Mapper không được phép sử dụng Shizuku. Nhấn để cấp quyền này. + 3. Key Mapper sẽ tự động sử dụng Shizuku. Nhấn để đọc tính năng Key Mapper nào sử dụng Shizuku. + Tùy chọn ánh xạ mặc định + Thay đổi các tùy chọn mặc định cho ánh xạ của bạn. + + + Tự động hiển thị hộp chọn bàn phím + Nhấn để xem cài đặt cho phép bạn tự động hiển thị bộ chọn bàn phím. + Thông báo + Mặc định + Cài đặt root + Các tùy chọn này sẽ chỉ hoạt động trên các thiết bị root! Nếu bạn không biết root là gì hoặc thiết bị của bạn đã được root hay chưa, vui lòng đừng để lại đánh giá kém nếu chúng không hoạt động. :) + Yêu cầu quyền WRITE_SECURE_SETTINGS + Các tùy chọn này chỉ được bật nếu Key Mapper có quyền WRITE_SECURE_SETTINGS. Nhấp vào nút bên dưới để tìm hiểu cách cấp quyền. + Hỗ trợ Shizuku + Shizuku là ứng dụng cho phép Key Mapper thực hiện những việc mà chỉ ứng dụng hệ thống mới làm được. Bạn không cần sử dụng bàn phím Key Mapper chẳng hạn. Nhấn để tìm hiểu cách thiết lập tính năng này. + Hãy làm theo các bước sau để thiết lập Shizuku. + Tự động thay đổi bàn phím + Đây là những cài đặt thực sự hữu ích và bạn nên kiểm tra chúng! + Ghi nhật ký + Điều này có thể làm tăng độ trễ cho bản đồ chính của bạn, vì vậy chỉ bật tính năng này nếu bạn đang cố gắng gỡ lỗi ứng dụng hoặc được nhà phát triển yêu cầu. + + + Nhật ký thay đổi + Giấy phép + Chính sách bảo mật + Tín dụng + Mã nguồn + Hồ sơ GitHub của nhà phát triển + Đánh giá và bình luận + Chủ đề XDA + Phiên bản + Dịch + Máy chủ Discord + Kênh YouTube (Hướng dẫn) + + + Hiển thị hộp thoại âm lượng + Nhấn giữ + Rung + Hiển thị thông báo trên màn hình + Rung khi nhấn phím lần đầu và rung lại khi nhấn lâu. + Phát hiện kích hoạt khi màn hình tắt + Lặp lại + %dx + sau %dms + mỗi %dms + cho đến khi vuốt lại + cho đến khi nhấn lại + cho đến khi được thả ra + Lặp lại + Lặp lại cho đến khi được thả ra + Lặp lại cho đến khi nhấn lại + Giữ và giữ + Giữ cho đến khi nhấn lại + Không ánh xạ lại + Giữ cho đến khi vuốt lại + Cho phép các ứng dụng khác kích hoạt bản đồ phím này + + + Dịch vụ trợ năng đã được bật :) + Tất cả đều tốt. Key Mapper hiện có thể phát hiện các lần nhấn nút của bạn. + Khởi động lại dịch vụ trợ năng + Các dịch vụ trợ năng đã được bật nhưng điện thoại của bạn đã tắt hoặc bị hỏng. Khởi động lại nó. + Khởi động lại + Kích hoạt dịch vụ trợ năng + @string/accessibility_service_explanation + Dịch vụ trợ năng bị vô hiệu hóa + Bạn chỉ có thể ghi lại trình kích hoạt nếu dịch vụ trợ năng được bật. + Lưu ý từ nhà phát triển + Không có gì đảm bảo rằng mọi hành động sẽ hoạt động trên thiết bị của bạn và mọi nút đều có thể được phát hiện. Điều này là do Android có nhiều phiên bản khác nhau và các OEM có thể vô tình hoặc cố ý phá vỡ các tính năng. Nếu có điều gì đó không ổn, vui lòng thông báo cho nhà phát triển và không đánh giá ứng dụng kém vì sự cố thường nằm ngoài tầm kiểm soát của nhà phát triển. =) + Key Mapper có thể ngừng hoạt động ngẫu nhiên! + PHÊ BÌNH!!! Nhấn \"tắt\" để hy vọng ngăn Android dừng ứng dụng khi ứng dụng này ở chế độ nền. Giao diện OEM của bạn như MIUI hoặc Samsung Experience có thể có các tính năng \"tiết kiệm pin\" khác, vì vậy bạn PHẢI tắt chúng cho Key Mapper bằng cách làm theo hướng dẫn tại Dontkillmyapp.com + Tối ưu hóa pin Android gốc đã tắt. Điều này không đủ tốt cho hầu hết các thiết bị, vì vậy hãy truy cập Dontkillmyapp.com để xem hướng dẫn cách tắt nhiều tính năng diệt ứng dụng hơn nữa trên thiết bị của bạn. + Tắt + Truy cập Dontkillmyapp.com + Ánh xạ lại các nút âm lượng? + Bạn có thể ánh xạ lại các nút âm lượng + Key Mapper cần có quyền truy cập Không làm phiền nếu bạn muốn các hành động thay đổi âm lượng và ánh xạ lại các nút âm lượng hoạt động. + Tất cả đều tốt! :) + Đóng góp + "Ứng dụng này là nguồn mở! Bạn có thể bắt đầu đóng góp bằng cách truy cập kho lưu trữ sds100/KeyMapper trên GitHub và bằng cách tham gia máy chủ Discord. Ngay cả khi bạn không thể viết mã, bạn vẫn có thể đóng góp bằng cách giúp đỡ người khác và thử nghiệm các tính năng mới nhất." + Nhấn vào dấu 3 chấm để thay đổi hành vi lặp lại và hơn thế nữa. Bạn có thể kiểm tra một hành động và sửa lỗi hành động bằng cách nhấn vào hành động đó. + Ánh xạ lại cử chỉ đọc dấu vân tay + Bạn có thể ánh xạ lại cử chỉ đọc dấu vân tay! :) + Bạn không thể ánh xạ lại cử chỉ đọc dấu vân tay! + Bạn cần bật dịch vụ trợ năng để Key Mapper có thể kiểm tra xem thiết bị của bạn có thể phát hiện cử chỉ vân tay hay không. + Thiết bị của bạn có thể phát hiện cử chỉ vân tay! Có một tab ở đầu màn hình chính để ánh xạ lại cử chỉ vân tay. + Thiết bị của bạn không cho phép ứng dụng của bên thứ 3 phát hiện cử chỉ vân tay! Nhà phát triển không thể làm gì về điều này. Một số thiết bị có cài đặt vuốt xuống trên đầu đọc dấu vân tay để mở ngăn thông báo và không cho phép ứng dụng bên thứ 3 phát hiện cử chỉ vân tay. + Cho phép + Bạn cần thiết lập lại một số cài đặt + Có vẻ như bạn đã sử dụng cài đặt để tự động thay đổi bàn phím hoặc hiển thị trình chọn phương thức nhập khi thiết bị Bluetooth kết nối và ngắt kết nối. Key Mapper hiện cho phép bạn sử dụng bất kỳ thiết bị nhập nào chứ không chỉ thiết bị Bluetooth. Không có cách nào để di chuyển cài đặt cũ theo cách mà chức năng mới sẽ hoạt động nên bạn sẽ phải chọn lại thiết bị. + Cấp phép cho Shizuku + Có vẻ như bạn đã cài đặt Shizuku. Bạn nên cấp quyền cho Key Mapper sử dụng Shizuku để Key Mapper có thể làm nhiều việc hơn mà không cần người dùng nhập liệu. Ví dụ như nhấn nút nhập liệu mà không cần bạn phải sử dụng \'bàn phím Key Mapper\'. Nhấn \'thêm thông tin\' để đọc tất cả các lợi ích. Nhấn \'cấp\' để cấp quyền. + Shizuku đã được phép! + Bạn đã cấp quyền thành công cho Key Mapper Shizuku. + Thông tin thêm + Khoản trợ cấp + Shizuku chưa bắt đầu + Shizuku phải được khởi động trước khi bạn cấp quyền cho Key Mapper sử dụng. Nhấn vào \'Khởi chạy Shizuku\' để mở ứng dụng Shizuku để bạn có thể khởi động ứng dụng. + Cài đặt Shizuku + Cấp quyền thông báo + Một số tính năng của Key Mapper yêu cầu quyền đăng thông báo, ví dụ: có thông báo tạm dừng/tiếp tục các bản đồ chính của bạn. Nhấn \'cấp\' để cấp quyền. + Thông báo có thể được hiển thị! + Bạn đã cấp thành công quyền Key Mapper để hiển thị thông báo. + Khoản trợ cấp + + + Khả năng tiếp cận + Báo thức + DTMF + Âm nhạc + Thông báo + Nhẫn + Hệ thống + Cuộc gọi thoại + Nhẫn + Rung + Im lặng + Đằng trước + Back + Báo động + Sự ưu tiên + Không có gì + + + Tạm dừng ánh xạ + Tiếp tục ánh xạ + Dịch vụ bị vô hiệu hóa + Dịch vụ trợ năng Key Mapper bị vô hiệu hóa + Dịch vụ bàn phím Key Mapper bị tắt + Chuyển đổi bàn phím Key Mapper + + + Ctrl + Ctrl left + Ctrl right + Alt + Alt trái + Alt phải + Shift + Shift trái + Shift phải + Meta + Meta trái + Meta phải + Sym + Func + Khóa mũ + Khóa số + Khóa cuộn + + + Bạn phải gõ một phím! + Bạn phải có ít nhất một trình kích hoạt + Bạn phải chọn một hành động! + Phím tắt phải có tiêu đề! + Bạn phải sử dụng một trong các bàn phím Key Mapper để tác vụ này hoạt động! + Không thể tìm thấy lối tắt. Ứng dụng đã được cài đặt hay kích hoạt chưa? + Ứng dụng có tên gói %s chưa được cài đặt! + Ứng dụng chưa được cài đặt! + Ứng dụng đã bị vô hiệu hóa! + Ứng dụng %s đã bị tắt! + Bạn cần cấp quyền cho Key Mapper để sửa đổi cài đặt hệ thống. + Điều này đòi hỏi sự cho phép root! + Hành động này cần có sự cho phép của máy ảnh! + Yêu cầu Android %s hoặc mới hơn + Yêu cầu Android %s trở lên + Thiết bị của bạn không có camera. + Thiết bị của bạn không hỗ trợ NFC. + Thiết bị của bạn không có đầu đọc dấu vân tay. + Thiết bị của bạn không hỗ trợ WiFi. + Thiết bị của bạn không hỗ trợ Bluetooth. + Thiết bị của bạn không hỗ trợ thực thi chính sách thiết bị. + Thiết bị của bạn không có đèn flash của máy ảnh. + Thiết bị của bạn không có bất kỳ tính năng điện thoại nào. + Không tìm thấy cờ \"%s\"! + Không thể tìm thấy trang cài đặt bàn phím! + Key Mapper cần phải là quản trị viên thiết bị! + Đang ở chế độ Không làm phiền! + Key Mapper không có quyền sử dụng phím tắt đó + Ứng dụng cần có quyền thay đổi trạng thái Không làm phiền! + Hành động này cần có quyền đọc trạng thái điện thoại! + Không thể tìm thấy trang cấp phép WRITE_SETTINGS! + Lỗi mở lối tắt ứng dụng này + Không có ứng dụng nào được cài đặt có thể gửi email! + Đã cấp quyền thay đổi chế độ Không làm phiền! + Không thể tìm thấy cài đặt quyền truy cập Không làm phiền! + Key Mapper cần có quyền WRITE_SECURE_SETTINGS. + Không thể tìm thấy ứng dụng nào để mở URL đó + Không thực hiện được lệnh \"getevent\". Bạn có chắc là bạn đã root chưa? + Không có ứng dụng nào có thể bắt đầu cuộc gọi điện thoại này + Máy ảnh đang được sử dụng! + Máy ảnh đã bị ngắt kết nối! + Máy ảnh bị vô hiệu hóa! + Lỗi máy ảnh! + Số lượng camera tối đa được sử dụng! + Không thể truy cập vào máy ảnh! + Không có đèn flash phía trước + Không có đèn flash phía sau + Dịch vụ trợ năng phải được kích hoạt để ứng dụng này hoạt động! + Dịch vụ trợ năng cần được kích hoạt! + Dịch vụ trợ năng đã được kích hoạt! + Dịch vụ trợ năng cần được kích hoạt! + Dịch vụ trợ năng cần được khởi động lại! + Trình khởi chạy của bạn không hỗ trợ phím tắt. + Một số tính năng cần có quyền WRITE_SECURE_SETTINGS. + Đã cấp quyền WRITE_SECURE_SETTINGS. + Phương thức nhập đã chọn của bạn cần được bật để “Sự kiện chính”, “Khóa”, “Văn bản” và một số hành động khác hoạt động. + Cần phải bật bàn phím Key Mapper! + Bàn phím Key Mapper đã được bật! + Bàn phím Key Mapper phải được bật và chọn để một số hành động của bạn hoạt động! + Không thể tìm thấy phương thức nhập %s + Không thể hiển thị bộ chọn phương thức nhập! + Không tìm thấy nút trợ năng! + Không thực hiện được hành động chung %s! + Bản đồ chính của bạn sẽ không hoạt động! Một số điều cần sửa chữa! + Bạn không thể di chuyển đến cuối văn bản trong trường này! + Không tìm thấy cài đặt tối ưu hóa pin! Nếu nó tồn tại, hãy mở nó bằng tay. + Không tìm thấy phần bổ sung (%s)! + Bạn không thể có những ràng buộc trùng lặp! + Cử chỉ này đã có hạn chế này! + Không thể trống rỗng! + Điều này không được hỗ trợ. :( + Không tìm thấy thiết bị! + Không thể chọn tập tin + Tệp JSON trống! + Quyền truy cập tập tin bị từ chối! %S + Lỗi IO không xác định! + Đã hủy! + Không thể sao lưu vào tập tin. Nó có bị xóa không? + Số không hợp lệ! + Ít nhất phải là %s! + Tối đa phải là %s! + Tối ưu hóa pin được bật! Hãy tắt tính năng này đi vì điều này có thể khiến Key Mapper ngừng hoạt động một cách ngẫu nhiên. + Điều này đòi hỏi sự cho phép root! + Không thể tìm thấy cài đặt truy cập thông báo! + Quyền truy cập thông báo bị từ chối! + Không hợp lệ! + Bị từ chối quyền bắt đầu cuộc gọi điện thoại! + Bạn sẽ cần cập nhật Key Mapper lên phiên bản mới nhất để sử dụng bản sao lưu này. + Tệp JSON bị hỏng! + Không có trợ lý giọng nói được cài đặt! + Không đủ quyền + Bạn chỉ cài đặt bàn phím Key Mapper! + Không có ứng dụng chơi phương tiện truyền thông! + Không tìm thấy tập tin nguồn! %S + Không tìm thấy tệp mục tiêu! %S + Không nhập được cử chỉ! + Không thể sửa đổi cài đặt hệ thống %s! + Bạn cần kích hoạt %s! + Không thể thay đổi ime! + Thiết bị của bạn không có ứng dụng camera! + Thiết bị của bạn không có trợ lý! + Thiết bị của bạn không có ứng dụng cài đặt! + Không ứng dụng nào có thể mở url này! + Không tạo được tập tin! + Không phải là một thư mục! %S + Không phải là một tập tin! %S + Không tìm thấy thư mục! %S + Không thể tìm thấy tập tin âm thanh! + Quyền lưu trữ bị từ chối! + Nguồn và đích không thể giống nhau! + Không còn khoảng trống ở mục tiêu! %S + Sự cho phép của Shizuku bị từ chối! + Shizuku chưa bắt đầu! + Tập tin này không có tên! + Bạn phải cấp quyền Key Mapper để xem các thiết bị Bluetooth đã ghép nối của mình. + Bị từ chối quyền đọc vị trí ! + Bị từ chối quyền trả lời và kết thúc cuộc gọi điện thoại! + Đã từ chối quyền xem các thiết bị Bluetooth được ghép nối! + Đã từ chối quyền hiển thị thông báo! + Phải từ 2 trở lên! + Phải bằng %d hoặc ít hơn! + Phải lớn hơn 0! + Phải lớn hơn 0! + Phải lớn hơn 0! + Phải lớn hơn 0! + Phải bằng %d hoặc ít hơn! + + + Chuyển đổi Wi-Fi + Bật Wi-Fi + Tắt Wi-Fi + Chuyển đổi Bluetooth + Bật Bluetooth + Tắt Bluetooth + Tăng âm lượng + Giảm âm lượng + Tắt âm lượng + Chuyển đổi tắt tiếng + Bật âm lượng + Hiển thị hộp thoại âm lượng + Tăng luồng + Tăng luồng %s + Giảm luồng + Giảm luồng %s + Chuyển qua các chế độ chuông (Rung, Rung, Im lặng) + Chuyển qua các chế độ chuông (Ring, Vibrate) + Thay đổi chế độ chuông + Thay đổi sang chế độ %s + Chuyển đổi chế độ Không làm phiền + Chỉ chuyển đổi chế độ DND %s + Bật chế độ Không làm phiền + Chỉ bật chế độ DND %s + Tắt chế độ Không làm phiền + Bật tự động xoay + Vô hiệu hóa tự động xoay + Chuyển đổi tự động xoay + Chế độ chân dung + Chế độ phong cảnh + Chuyển hướng + Xoay vòng qua các vòng quay + Xoay vòng qua %s vòng quay + Chuyển đổi dữ liệu di động + Kích hoạt dữ liệu di động + Tắt dữ liệu di động + Chuyển đổi độ sáng tự động + Tắt độ sáng tự động + Bật độ sáng tự động + Tăng độ sáng màn hình + Giảm độ sáng màn hình + Mở rộng ngăn thông báo + Chuyển đổi ngăn thông báo + Mở rộng cài đặt nhanh + Chuyển đổi ngăn cài đặt nhanh + Thu gọn thanh trạng thái + Tạm dừng phát lại phương tiện + Tạm dừng phát lại phương tiện cho một ứng dụng + Tạm dừng phương tiện trong %s + Tiếp tục phát lại phương tiện + Tiếp tục phát lại phương tiện cho một ứng dụng + Tiếp tục phương tiện cho %s + Phát/Tạm dừng phát lại phương tiện + Phát/Tạm dừng phát lại phương tiện cho một ứng dụng + Phát/Tạm dừng phương tiện trong %s + Bài hát tiếp theo + Bản nhạc tiếp theo cho một ứng dụng + Bài hát tiếp theo cho %s + Bản nhạc trước + Bản nhạc trước của một ứng dụng + Bài hát trước đó của %s + Chuyển tiếp nhanh + Chuyển tiếp nhanh cho một ứng dụng + Chuyển tiếp nhanh trong %s + Không phải tất cả các ứng dụng đa phương tiện đều hỗ trợ chuyển tiếp nhanh. Ví dụ: Google Play Âm nhạc. + Tua lại + Tua lại cho một ứng dụng + Tua lại trong %s + Không phải tất cả các ứng dụng media đều hỗ trợ tua lại. Ví dụ: Google Play Âm nhạc. + Quay lại + Go home + Mở gần đây + Mở trình đơn + Chuyển đổi màn hình chia nhỏ + Chuyển đến ứng dụng cuối cùng. (Nhấn đúp vào phần gần đây) + Bật tắt đèn pin + Bật đèn pin + Tắt đèn pin + Chuyển đổi đèn pin %s + Bật đèn pin %s + Tắt đèn pin %s + Bật NFC + Tắt NFC + Chuyển đổi NFC + Ảnh chụp màn hình + Khởi chạy trợ lý giọng nói + Khởi chạy trợ lý thiết bị + Mở máy ảnh + Khóa thiết bị + Thiết bị khóa an toàn + Bạn sẽ chỉ có thể đăng nhập lại bằng mã PIN của mình. Máy quét dấu vân tay và mở khóa bằng khuôn mặt sẽ bị tắt. Đây là cách đáng tin cậy duy nhất mà tôi tìm thấy để khóa các thiết bị chưa root trước Android Pie 9.0. + Thiết bị ngủ/thức + Bạn phải bật tùy chọn phát hiện trigger khi màn hình tắt! + Không làm gì cả + Di chuyển con trỏ đến cuối + Hành động này có thể không hoạt động như dự định trong một số ứng dụng. + Chuyển đổi bàn phím + Hành động này sẽ chỉ hoạt động nếu bạn đã nhấn vào trường nhập liệu nơi bàn phím được cho là sẽ hiển thị. + Hiển thị bàn phím + Ẩn bàn phím + Hiển thị hộp chọn bàn phím + Chuyển đổi bàn phím + Chuyển sang %s + Cắt + Sao chép + Dán + Chọn từ tại con trỏ + Mở cài đặt + Hiển thị menu nguồn + Chuyển đổi chế độ trên máy bay + Bật chế độ trên máy bay + Tắt chế độ Máy bay + Khởi chạy ứng dụng + Khởi chạy phím tắt ứng dụng + Nhập mã khóa + Sự kiện phím đầu vào + Nhấn vào màn hình + Vuốt màn hình + Chụm màn hình + Nhập văn bản + Mở URL + Gửi ý định + Bắt đầu cuộc gọi điện thoại + Trả lời cuộc gọi điện thoại + Kết thúc cuộc gọi điện thoại + Phát âm thanh + Loại bỏ thông báo gần đây nhất + Loại bỏ tất cả thông báo + + + Điều hướng + Âm lượng + Phương tiện truyền thông + Bàn phím + Ứng dụng + Đầu vào + Máy ảnh & Âm thanh + Kết nối + Nội dung + Giao diện + Điện thoại + Trưng bày + Thông báo + + + Boolean + Mảng Boolean + Số nguyên + Mảng số nguyên + Chuỗi + Mảng chuỗi + Dài + Mảng dài + Byte + Mảng byte + Double + Mảng đôi + Char + Mảng char + Nổi + Mảng nổi + Ngắn + Mảng ngắn + Chỉ có thể là \"đúng\" hoặc \"sai\" + Danh sách \"đúng\" và \"sai\" được phân tách bằng dấu phẩy. Ví dụ: đúng, sai, đúng + Một số nguyên hợp lệ trong ngôn ngữ lập trình Java. + Danh sách các số nguyên hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 100.399 + Một danh sách được phân tách bằng dấu phẩy. Ví dụ: loại 1, loại 2 + Bất kỳ văn bản nào. + Danh sách các chuỗi được phân tách bằng dấu phẩy. Ví dụ: chuỗi1, chuỗi2 + Một Long hợp lệ trong ngôn ngữ lập trình Java. + Danh sách các Độ dài hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 102302234234234,399083423234429 + Một Byte hợp lệ trong ngôn ngữ lập trình Java. + Danh sách các Byte hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 123,3 + Một Double hợp lệ trong ngôn ngữ lập trình Java. + Danh sách các Nhân đôi hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 1.0,3.234 + Một Char hợp lệ trong ngôn ngữ lập trình Java. Ví dụ: \'a\' hoặc \'b\' + Danh sách các ký tự hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: a,b,c + Float hợp lệ trong ngôn ngữ lập trình Java. Ví dụ: 3,145 + Danh sách các Float hợp lệ được phân tách bằng dấu phẩy trong ngôn ngữ lập trình Java. Ví dụ: 1241.123 + Một đoạn ngắn hợp lệ bằng ngôn ngữ lập trình Java. Ví dụ: 2342 + Danh sách các video ngắn hợp lệ được phân tách bằng dấu phẩy bằng ngôn ngữ lập trình Java. Ví dụ: 3242,12354 + Các cờ cho một Ý định được lưu dưới dạng cờ bit. Những cờ này thay đổi cách xử lý Ý định. Nếu mục này trống đối với Ý định hoạt động thì Trình ánh xạ khóa sẽ sử dụng FLAG_ACTIVITY_NEW_TASK theo mặc định. Để biết thêm thông tin, hãy nhấn vào \'tài liệu\' để xem tài liệu dành cho nhà phát triển Android. + + + Hướng dẫn bắt đầu nhanh + Hãy xem Hướng dẫn bắt đầu nhanh nếu bạn gặp khó khăn. + + + GitHub + Website + Bản dịch + Phiên bản %s + Tỷ lệ + Nhật ký thay đổi + Discord + Những thứ nhàm chán + Giấy phép + Giấy phép nguồn mở cho ứng dụng này. + Chính sách bảo mật + Chúng tôi không thu thập bất kỳ thông tin cá nhân nào nhưng đây là chính sách bảo mật nói lên điều này. + Đội ngũ của chúng tôi + Nhà phát triển + Người điều hành/hỗ trợ cộng đồng + Người điều hành/hỗ trợ cộng đồng + Phiên dịch viên (tiếng Ba Lan) + Người phiên dịch (tiếng Séc) + Phiên dịch viên (tiếng Tây Ban Nha) + + diff --git a/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt deleted file mode 100644 index c906eb2d3a..0000000000 --- a/app/src/test/java/io/github/sds100/keymapper/AutoSwitchImeControllerTest.kt +++ /dev/null @@ -1,224 +0,0 @@ -package io.github.sds100.keymapper - -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.FakePreferenceRepository -import io.github.sds100.keymapper.mappings.PauseMappingsUseCase -import io.github.sds100.keymapper.system.devices.FakeDevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputmethod.AutoSwitchImeController -import io.github.sds100.keymapper.system.inputmethod.ImeInfo -import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import io.github.sds100.keymapper.system.popup.PopupMessageAdapter -import io.github.sds100.keymapper.util.ServiceEvent -import io.github.sds100.keymapper.util.Success -import io.github.sds100.keymapper.util.ui.ResourceProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -/** - * Created by sds100 on 25/04/2021. - */ - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -class AutoSwitchImeControllerTest { - - companion object { - private const val KEY_MAPPER_IME_ID = "key_mapper_keyboard_id" - private const val NORMAL_IME_ID = "proper_keyboard_id" - - private val FAKE_KEYBOARD = InputDeviceInfo( - descriptor = "fake_keyboard_descriptor", - name = "fake keyboard", - id = 1, - isExternal = true, - isGameController = false, - ) - - private val FAKE_CONTROLLER = InputDeviceInfo( - descriptor = "fake_controller_descriptor", - name = "fake controller", - id = 2, - isExternal = true, - isGameController = true, - ) - - private val KEY_MAPPER_IME = ImeInfo( - id = KEY_MAPPER_IME_ID, - packageName = Constants.PACKAGE_NAME, - label = "label", - isEnabled = true, - isChosen = false, - ) - - private val NORMAL_IME = ImeInfo( - id = NORMAL_IME_ID, - packageName = "other.example.app", - label = "normal keyboard", - isEnabled = true, - isChosen = true, - ) - } - - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) - - private lateinit var controller: AutoSwitchImeController - private lateinit var fakePreferenceRepository: FakePreferenceRepository - private lateinit var mockInputMethodAdapter: InputMethodAdapter - private lateinit var mockPauseMappingsUseCase: PauseMappingsUseCase - private lateinit var fakeDevicesAdapter: FakeDevicesAdapter - private lateinit var mockPopupMessageAdapter: PopupMessageAdapter - private lateinit var mockResourceProvider: ResourceProvider - - @Before - fun init() { - fakePreferenceRepository = FakePreferenceRepository() - - mockInputMethodAdapter = mock { - on { getInfoByPackageName(Constants.PACKAGE_NAME) }.then { - Success(KEY_MAPPER_IME) - } - - on { inputMethodHistory }.then { - MutableStateFlow( - listOf(NORMAL_IME), - ) - } - - onBlocking { chooseImeWithoutUserInput(KEY_MAPPER_IME_ID) }.then { - Success( - KEY_MAPPER_IME, - ) - } - onBlocking { chooseImeWithoutUserInput(NORMAL_IME_ID) }.then { - Success( - NORMAL_IME, - ) - } - } - - fakeDevicesAdapter = FakeDevicesAdapter() - - mockPopupMessageAdapter = mock() - - mockPauseMappingsUseCase = mock { - on { isPaused }.then { flow { } } - } - - mockResourceProvider = mock() - - controller = AutoSwitchImeController( - coroutineScope, - fakePreferenceRepository, - mockInputMethodAdapter, - mockPauseMappingsUseCase, - fakeDevicesAdapter, - mockPopupMessageAdapter, - mockResourceProvider, - accessibilityServiceAdapter = mock { - on { eventReceiver }.then { MutableSharedFlow() } - }, - ) - } - - @Test - fun `choose single device, when device connected, show ime picker`() = - coroutineScope.runBlockingTest { - // GIVEN - val chosenDevices = setOf(FAKE_KEYBOARD.descriptor) - - fakePreferenceRepository.set(Keys.showImePickerOnDeviceConnect, true) - fakePreferenceRepository.set(Keys.devicesThatShowImePicker, chosenDevices) - - // WHEN - fakeDevicesAdapter.onInputDeviceConnect.emit(FAKE_KEYBOARD) - - // THEN - verify(mockInputMethodAdapter, times(1)).showImePicker(fromForeground = false) - } - - @Test - fun `choose single device, when device disconnected, show ime picker`() = - coroutineScope.runBlockingTest { - // GIVEN - val chosenDevices = setOf(FAKE_KEYBOARD.descriptor) - - fakePreferenceRepository.set(Keys.showImePickerOnDeviceConnect, true) - fakePreferenceRepository.set(Keys.devicesThatShowImePicker, chosenDevices) - - // WHEN - fakeDevicesAdapter.onInputDeviceDisconnect.emit(FAKE_KEYBOARD) - - // THEN - verify(mockInputMethodAdapter, times(1)).showImePicker(fromForeground = false) - } - - @Test - fun `choose single device, on device disconnect, choose normal keyboard`() = - coroutineScope.runBlockingTest { - // GIVEN - val chosenDevices = setOf(FAKE_KEYBOARD.descriptor) - fakePreferenceRepository.set(Keys.devicesThatChangeIme, chosenDevices) - fakePreferenceRepository.set(Keys.changeImeOnDeviceConnect, true) - fakePreferenceRepository.set(Keys.showToastWhenAutoChangingIme, true) - - whenever(mockInputMethodAdapter.chosenIme).then { MutableStateFlow(KEY_MAPPER_IME) } - - // WHEN - fakeDevicesAdapter.onInputDeviceDisconnect.emit(FAKE_KEYBOARD) - - // THEN - verify(mockInputMethodAdapter, times(1)).chooseImeWithoutUserInput( - NORMAL_IME_ID, - ) - - verify(mockResourceProvider, times(1)).getString( - R.string.toast_chose_keyboard, - NORMAL_IME.label, - ) - verify(mockPopupMessageAdapter, times(1)).showPopupMessage(anyOrNull()) - } - - @Test - fun `choose single device, when device connected, choose key mapper keyboard`() = - coroutineScope.runBlockingTest { - // GIVEN - val chosenDevices = setOf(FAKE_KEYBOARD.descriptor) - fakePreferenceRepository.set(Keys.devicesThatChangeIme, chosenDevices) - fakePreferenceRepository.set(Keys.changeImeOnDeviceConnect, true) - fakePreferenceRepository.set(Keys.showToastWhenAutoChangingIme, true) - - whenever(mockInputMethodAdapter.chosenIme).then { MutableStateFlow(NORMAL_IME) } - - // WHEN - fakeDevicesAdapter.onInputDeviceConnect.emit(FAKE_KEYBOARD) - - // THEN - verify(mockInputMethodAdapter, times(1)).chooseImeWithoutUserInput( - KEY_MAPPER_IME_ID, - ) - - verify(mockResourceProvider, times(1)).getString( - R.string.toast_chose_keyboard, - KEY_MAPPER_IME.label, - ) - verify(mockPopupMessageAdapter, times(1)).showPopupMessage(anyOrNull()) - } -} 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 3deb16da96..2874f2287d 100644 --- a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt @@ -25,12 +25,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.test.DelayController -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` @@ -51,7 +50,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import timber.log.Timber import java.io.File -import kotlin.coroutines.ContinuationInterceptor /** * Created by sds100 on 19/04/2021. @@ -65,9 +63,8 @@ class BackupManagerTest { @get:Rule var temporaryFolder = TemporaryFolder() - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private val dispatcherProvider = TestDispatcherProvider(testDispatcher) @@ -83,7 +80,9 @@ class BackupManagerTest { private lateinit var gson: Gson @Before - fun init() { + fun setUp() { + Dispatchers.setMain(testDispatcher) + Timber.plant(TestLoggingTree()) fakePreferenceRepository = FakePreferenceRepository() @@ -105,7 +104,7 @@ class BackupManagerTest { mockUuidGenerator = mock() backupManager = BackupManagerImpl( - coroutineScope, + testScope, fileAdapter = fakeFileAdapter, keyMapRepository = mockKeyMapRepository, preferenceRepository = fakePreferenceRepository, @@ -118,14 +117,11 @@ class BackupManagerTest { parser = JsonParser() gson = Gson() - - Dispatchers.setMain(testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() } /** @@ -133,7 +129,9 @@ class BackupManagerTest { */ @Test fun `Don't allow back ups from a newer version of key mapper`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { + advanceUntilIdle() + // GIVEN val dataJsonFile = "restore-app-version-too-big.zip/data.json" val zipFile = fakeFileAdapter.getPrivateFile("backup.zip") @@ -141,14 +139,11 @@ class BackupManagerTest { copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json") // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(zipFile.uri) + advanceUntilIdle() // THEN assertThat(result, `is`(Error.BackupVersionTooNew)) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() } /** @@ -156,7 +151,7 @@ class BackupManagerTest { */ @Test fun `Allow back ups from a back up without a key mapper version in it`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN whenever(mockKeyMapRepository.keyMapList).then { MutableStateFlow(State.Data(emptyList())) @@ -172,18 +167,15 @@ class BackupManagerTest { copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json") // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() val result = backupManager.restoreMappings(zipFile.uri) // THEN assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() } @Test - fun `don't crash if back up does not contain sounds folder`() = coroutineScope.runBlockingTest { + fun `don't crash if back up does not contain sounds folder`() = runTest(testDispatcher) { // GIVEN whenever(mockKeyMapRepository.keyMapList).then { MutableStateFlow(State.Data(emptyList())) @@ -199,19 +191,15 @@ class BackupManagerTest { copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json") // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(zipFile.uri) // THEN assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() } @Test fun `successfully restore zip folder with data json and sound files`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val dataJsonFile = "restore-all.zip/data.json" val soundFile = "restore-all.zip/sounds/sound.ogg" @@ -221,15 +209,11 @@ class BackupManagerTest { copyFileToPrivateFolder(soundFile, destination = "backup.zip/sounds/sound.ogg") // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(zipFile.uri) // THEN assertThat(result, `is`(Success(Unit))) - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockKeyMapRepository, times(1)).insert(any(), any()) verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any()) verify(mockSoundsManager, times(1)).restoreSound(any()) @@ -240,7 +224,7 @@ class BackupManagerTest { */ @Test fun `backup sound file even if there is not a key map with a sound action`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val backupDirUuid = "backup_uid" val soundFileName = "sound.ogg" @@ -270,19 +254,14 @@ class BackupManagerTest { } // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val backupZip = File(temporaryFolder.root, "backup.zip") backupZip.mkdirs() val result = backupManager.backupMappings(uri = backupZip.path) // THEN - assertThat(result, `is`(Success(backupZip.path))) - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - // only 2 files have been backed up assertThat(backupZip.listFiles()?.size, `is`(2)) @@ -294,7 +273,7 @@ class BackupManagerTest { @Test fun `backup sound file if there is a key map with a sound action`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val backupDirUuid = "backup_uuid" val soundFileUid = "uid" @@ -328,8 +307,6 @@ class BackupManagerTest { soundFile.createFile() // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val backupZip = File(temporaryFolder.root, "backup.zip") backupZip.mkdirs() @@ -339,8 +316,6 @@ class BackupManagerTest { assertThat(result, `is`(Success(backupZip.path))) - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - // only 2 files have been backed up assertThat(backupZip.listFiles()?.size, `is`(2)) @@ -352,145 +327,106 @@ class BackupManagerTest { } @Test - fun `restore legacy backup with device info, success`() = coroutineScope.runBlockingTest { + fun `restore legacy backup with device info, success`() = runTest(testDispatcher) { // GIVEN val fileName = "legacy-backup-test-data.json" // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) // THEN assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockKeyMapRepository, times(1)).insert(any(), any()) verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any()) } @Test fun `restore keymaps with no db version, assume version is 9 and don't show error message`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-keymaps-no-db-version.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockKeyMapRepository, times(1)).insert(any(), any()) } @Test fun `restore a single legacy fingerprint map, only restore a single fingerprint map and a success message`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-legacy-single-fingerprint-map.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockFingerprintMapRepository, times(1)).update(any()) } @Test fun `restore all legacy fingerprint maps, all fingerprint maps should be restored and a success message`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-all-legacy-fingerprint-maps.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockFingerprintMapRepository, times(1)).update(any(), any(), any(), any()) } @Test fun `restore many key maps and device info, all key maps and device info should be restored and a success message`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-many-keymaps.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Success(Unit))) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockKeyMapRepository, times(1)).insert(any(), any(), any(), any()) } @Test fun `restore with key map db version greater than allowed version, send incompatible backup event`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-keymap-db-version-too-big.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Error.BackupVersionTooNew)) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockKeyMapRepository, never()).insert(anyVararg()) } @Test fun `restore with legacy fingerprint gesture map db version greater than allowed version, send incompatible backup event`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val fileName = "restore-legacy-fingerprint-map-version-too-big.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Error.BackupVersionTooNew)) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - verify(mockFingerprintMapRepository, never()).update(anyVararg()) } @Test - fun `restore empty file, show empty json error message`() = coroutineScope.runBlockingTest { + fun `restore empty file, show empty json error message`() = runTest(testDispatcher) { val fileName = "empty.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, `is`(Error.EmptyJson)) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() } @Test - fun `restore corrupt file, show corrupt json message`() = coroutineScope.runBlockingTest { + fun `restore corrupt file, show corrupt json message`() = runTest(testDispatcher) { val fileName = "corrupt.json" - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() val result = backupManager.restoreMappings(copyFileToPrivateFolder(fileName)) assertThat(result, IsInstanceOf(Error.CorruptJsonFile::class.java)) - - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() } @Test fun `backup all fingerprint maps, return list of fingerprint maps and app database version`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val backupDirUuid = "backup_uuid" @@ -513,15 +449,11 @@ class BackupManagerTest { backupZip.mkdirs() // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.backupFingerprintMaps(backupZip.path) // THEN assertThat(result, `is`(Success(backupZip.path))) - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - // only 1 file has been backed up assertThat(backupZip.listFiles()?.size, `is`(1)) @@ -543,9 +475,8 @@ class BackupManagerTest { @Test fun `backup key maps, return list of default key maps, keymap db version should be current database version`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN - val backupDirUuid = "backup_uuid" whenever(mockUuidGenerator.random()).then { @@ -560,15 +491,11 @@ class BackupManagerTest { backupZip.mkdirs() // WHEN - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - val result = backupManager.backupKeyMaps(backupZip.path, keyMapList.map { it.uid }) // THEN assertThat(result, `is`(Success(backupZip.path))) - (coroutineScope.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() - // only 1 file has been backed up assertThat(backupZip.listFiles()?.size, `is`(1)) diff --git a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt index 67b541875c..f412bc7d8d 100644 --- a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt @@ -13,14 +13,11 @@ import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.singleKeyTrigger import io.github.sds100.keymapper.util.triggerKey import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.`is` -import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -32,9 +29,7 @@ import org.mockito.kotlin.mock @ExperimentalCoroutinesApi class ConfigKeyMapUseCaseTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() private lateinit var useCase: ConfigKeyMapUseCaseImpl @@ -47,11 +42,6 @@ class ConfigKeyMapUseCaseTest { ) } - @After - fun tearDown() { - testDispatcher.cleanupTestCoroutines() - } - /** * Issue #753. If a modifier key is used as a trigger then it the * option to not override the default action must be chosen so that the modifier @@ -59,7 +49,7 @@ class ConfigKeyMapUseCaseTest { */ @Test fun `when add modifier key trigger, enable do not remap option`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val modifierKeys = setOf( KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, @@ -93,7 +83,7 @@ class ConfigKeyMapUseCaseTest { */ @Test fun `when add non-modifier key trigger, do ont enable do not remap option`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN useCase.mapping.value = State.Data(KeyMap()) @@ -112,7 +102,7 @@ class ConfigKeyMapUseCaseTest { */ @Test fun `when add answer phone call action, then add phone ringing constraint`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN useCase.mapping.value = State.Data(KeyMap()) val action = ActionData.AnswerCall @@ -131,7 +121,7 @@ class ConfigKeyMapUseCaseTest { */ @Test fun `when add end phone call action, then add in phone call constraint`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN useCase.mapping.value = State.Data(KeyMap()) val action = ActionData.EndCall @@ -149,7 +139,7 @@ class ConfigKeyMapUseCaseTest { */ @Test fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val action = KeyMapAction( data = ActionData.TapScreen(100, 100, null), @@ -171,7 +161,7 @@ class ConfigKeyMapUseCaseTest { @Test fun `add modifier key event action, enable hold down option and disable repeat option`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> useCase.mapping.value = State.Data(KeyMap()) diff --git a/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt b/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt index b05e653a77..3c028b19b1 100644 --- a/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/KeyMapJsonMigrationTest.kt @@ -13,10 +13,9 @@ import io.github.sds100.keymapper.data.migration.Migration9To10 import io.github.sds100.keymapper.data.migration.MigrationUtils import io.github.sds100.keymapper.util.JsonTestUtils import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -42,9 +41,8 @@ class KeyMapJsonMigrationTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var parser: JsonParser private lateinit var gson: Gson @@ -111,7 +109,7 @@ class KeyMapJsonMigrationTest { expectedData: JsonArray, inputVersion: Int, outputVersion: Int, - ) = coroutineScope.runBlockingTest { + ) = runTest(testDispatcher) { val migrations = listOf( JsonMigration(9, 10) { json -> Migration9To10.migrateJson(json) }, JsonMigration(10, 11) { json -> Migration10To11.migrateJson(json) }, diff --git a/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt b/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt index 48260c24b0..8c5f6813f6 100644 --- a/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/LegacyFingerprintMapMigrationTest.kt @@ -14,10 +14,9 @@ import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintMapM import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintMapMigration1To2 import io.github.sds100.keymapper.util.JsonTestUtils import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -45,9 +44,9 @@ class LegacyFingerprintMapMigrationTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private lateinit var parser: JsonParser private lateinit var gson: Gson @@ -97,7 +96,7 @@ class LegacyFingerprintMapMigrationTest { expectedData: JsonArray, inputVersion: Int, outputVersion: Int, - ) = coroutineScope.runBlockingTest { + ) = runTest(testDispatcher) { val migrations = listOf( JsonMigration(0, 1) { json -> FingerprintMapMigration0To1.migrate(json) }, JsonMigration(1, 2) { json -> FingerprintMapMigration1To2.migrate(json) }, diff --git a/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt index ad298dcfac..1d5f11890f 100644 --- a/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/NotificationControllerTest.kt @@ -6,17 +6,15 @@ import io.github.sds100.keymapper.system.accessibility.ServiceState import io.github.sds100.keymapper.system.notifications.ManageNotificationsUseCase import io.github.sds100.keymapper.system.notifications.NotificationController import io.github.sds100.keymapper.system.notifications.NotificationModel -import io.github.sds100.keymapper.util.FlowUtils.toListWithTimeout import io.github.sds100.keymapper.util.ui.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.DelayController -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.junit.Before @@ -27,7 +25,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import kotlin.coroutines.ContinuationInterceptor /** * Created by sds100 on 25/04/2021. @@ -37,9 +34,8 @@ import kotlin.coroutines.ContinuationInterceptor @RunWith(MockitoJUnitRunner::class) class NotificationControllerTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var controller: NotificationController private lateinit var mockManageNotifications: ManageNotificationsUseCase @@ -62,7 +58,7 @@ class NotificationControllerTest { fakeOnboarding = FakeOnboardingUseCase() controller = NotificationController( - coroutineScope, + testScope, mockManageNotifications, pauseMappings = mock { on { isPaused }.then { flow {} } @@ -88,24 +84,20 @@ class NotificationControllerTest { @Test fun `click setup chosen devices notification, open app and approve`() = - coroutineScope.runBlockingTest { - // WHEN - - (coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher() - launch { - onActionClick.emit(NotificationController.ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN) + runTest(testDispatcher) { + val value = async { + controller.openApp.first() } - // THEN - assertThat(controller.openApp.toListWithTimeout().size, `is`(1)) - (coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher() + onActionClick.emit(NotificationController.ACTION_ON_SETUP_CHOSEN_DEVICES_AGAIN) + assertThat(value.await(), `is`("")) assertThat(fakeOnboarding.approvedSetupChosenDevicesAgainNotification, `is`(true)) } @Test fun `show setup chosen devices notification`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val title = "title" val text = "text" diff --git a/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt b/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt index 710e14e85b..f1384aba31 100644 --- a/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt +++ b/app/src/test/java/io/github/sds100/keymapper/TestDispatcherProvider.kt @@ -1,14 +1,14 @@ package io.github.sds100.keymapper import io.github.sds100.keymapper.util.DispatcherProvider -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestDispatcher /** * Created by sds100 on 01/05/2021. */ class TestDispatcherProvider( - private val testDispatcher: TestCoroutineDispatcher, + private val testDispatcher: TestDispatcher, ) : DispatcherProvider { override fun main() = testDispatcher override fun default() = testDispatcher diff --git a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt index d901f96a33..eca3be64c8 100644 --- a/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/actions/GetActionErrorUseCaseTest.kt @@ -8,10 +8,9 @@ import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.util.Error import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.nullValue @@ -30,9 +29,8 @@ import org.mockito.kotlin.whenever @RunWith(MockitoJUnitRunner::class) class GetActionErrorUseCaseTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var useCase: GetActionErrorUseCaseImpl @@ -61,56 +59,58 @@ class GetActionErrorUseCaseTest { * #776 */ @Test - fun `dont show Shizuku errors if a compatible ime is selected`() = coroutineScope.runBlockingTest { - // GIVEN - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } - whenever(mockInputMethodAdapter.chosenIme).then { - MutableStateFlow( - ImeInfo( - id = "ime_id", - packageName = "io.github.sds100.keymapper.inputmethod.latin", - label = "Key Mapper GUI Keyboard", - isEnabled = true, - isChosen = true, - ), - ) + fun `don't show Shizuku errors if a compatible ime is selected`() = + testScope.runTest { + // GIVEN + whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } + whenever(mockInputMethodAdapter.chosenIme).then { + MutableStateFlow( + ImeInfo( + id = "ime_id", + packageName = "io.github.sds100.keymapper.inputmethod.latin", + label = "Key Mapper GUI Keyboard", + isEnabled = true, + isChosen = true, + ), + ) + } + + val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + + // WHEN + val error = useCase.getError(action) + + // THEN + assertThat(error, nullValue()) } - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - - // WHEN - val error = useCase.getError(action) - - // THEN - assertThat(error, nullValue()) - } - /** * #776 */ @Test - fun `show Shizuku errors if a compatible ime is not selected and Shizuku is installed`() = coroutineScope.runBlockingTest { - // GIVEN - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } - whenever(mockShizukuAdapter.isStarted).then { MutableStateFlow(false) } - - whenever(mockInputMethodAdapter.chosenIme).then { - MutableStateFlow( - ImeInfo( - id = "ime_id", - packageName = "io.gboard", - label = "Gboard", - isEnabled = true, - isChosen = true, - ), - ) + fun `show Shizuku errors if a compatible ime is not selected and Shizuku is installed`() = + testScope.runTest { + // GIVEN + whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } + whenever(mockShizukuAdapter.isStarted).then { MutableStateFlow(false) } + + whenever(mockInputMethodAdapter.chosenIme).then { + MutableStateFlow( + ImeInfo( + id = "ime_id", + packageName = "io.gboard", + label = "Gboard", + isEnabled = true, + isChosen = true, + ), + ) + } + + val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + // WHEN + val error = useCase.getError(action) + + // THEN + assertThat(error, `is`(Error.ShizukuNotStarted)) } - - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - // WHEN - val error = useCase.getError(action) - - // THEN - assertThat(error, `is`(Error.ShizukuNotStarted)) - } } 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 6127261d01..483d51af3c 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 @@ -12,10 +12,9 @@ import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -37,9 +36,8 @@ import org.mockito.kotlin.whenever @RunWith(MockitoJUnitRunner::class) class PerformActionsUseCaseTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var useCase: PerformActionsUseCaseImpl private lateinit var mockKeyMapperImeMessenger: KeyMapperImeMessenger @@ -55,7 +53,7 @@ class PerformActionsUseCaseTest { mockToastAdapter = mock() useCase = PerformActionsUseCaseImpl( - coroutineScope, + testScope, accessibilityService = mockAccessibilityService, inputMethodAdapter = mock(), fileAdapter = mock(), @@ -95,7 +93,7 @@ class PerformActionsUseCaseTest { */ @Test fun `dont show accessibility service not found error for open menu action`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionData.OpenMenu @@ -118,7 +116,7 @@ class PerformActionsUseCaseTest { */ @Test fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val fakeGamePad = InputDeviceInfo( descriptor = "game_pad", @@ -156,7 +154,7 @@ class PerformActionsUseCaseTest { */ @Test fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) @@ -186,7 +184,7 @@ class PerformActionsUseCaseTest { */ @Test fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val fakeGamePad = InputDeviceInfo( descriptor = "game_pad", @@ -236,7 +234,7 @@ class PerformActionsUseCaseTest { */ @Test fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val descriptor = "fake_device_descriptor" @@ -271,9 +269,7 @@ class PerformActionsUseCaseTest { ) // none of the devices support the key code - fakeDevicesAdapter.deviceHasKey = { id, keyCode -> - false - } + fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } // WHEN useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) @@ -293,7 +289,7 @@ class PerformActionsUseCaseTest { @Test fun `perform key event action with no device name, ensure action is still performed with correct device id`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val descriptor = "fake_device_descriptor" 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 b223aed38d..e28c7f0db1 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 @@ -7,12 +7,11 @@ import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.createTestCoroutineScope import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` @@ -36,9 +35,9 @@ class ConfigKeyServiceEventActionViewModelTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private lateinit var viewModel: ConfigKeyEventActionViewModel private lateinit var mockUseCase: ConfigKeyEventUseCase @@ -63,13 +62,12 @@ class ConfigKeyServiceEventActionViewModelTest { @After fun tearDown() { - testDispatcher.cleanupTestCoroutines() Dispatchers.resetMain() } @Test fun `multiple input devices with same descriptor but a different name, choose a device, ensure device with correct name is chosen`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val fakeDevice1 = InputDeviceInfo( descriptor = "bla", @@ -92,7 +90,7 @@ class ConfigKeyServiceEventActionViewModelTest { // THEN viewModel.chooseDevice(0) - coroutineScope.advanceUntilIdle() + testScope.advanceUntilIdle() assertThat(viewModel.uiState.value.chosenDeviceName, `is`(fakeDevice1.name)) diff --git a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt index 5da322de5f..2a69ebf06e 100644 --- a/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/data/repositories/FingerprintMapRepositoryTest.kt @@ -11,10 +11,9 @@ import io.github.sds100.keymapper.util.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,9 +42,8 @@ class FingerprintMapRepositoryTest { ) } - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private val dispatchers = TestDispatcherProvider(testDispatcher) private lateinit var repository: RoomFingerprintMapRepository @@ -65,7 +63,7 @@ class FingerprintMapRepositoryTest { repository = RoomFingerprintMapRepository( mockDao, - coroutineScope, + testScope, devicesAdapter, dispatchers = dispatchers, ) @@ -73,8 +71,8 @@ class FingerprintMapRepositoryTest { @Test fun `only swipe down fingerprint map in database, insert 3 blank fingerprint maps for the other fingerprint maps`() = - coroutineScope.runBlockingTest { - repository.fingerprintMapList.launchIn(coroutineScope) + runTest(testDispatcher) { + repository.fingerprintMapList.launchIn(testScope) fingerprintMaps.emit(listOf(FingerprintMapEntity(id = FingerprintMapEntity.ID_SWIPE_DOWN))) @@ -87,8 +85,8 @@ class FingerprintMapRepositoryTest { @Test fun `no fingerprint maps in database, insert 4 blank fingerprint maps`() = - coroutineScope.runBlockingTest { - repository.fingerprintMapList.launchIn(coroutineScope) + runTest(testDispatcher) { + repository.fingerprintMapList.launchIn(testScope) fingerprintMaps.emit(emptyList()) @@ -102,7 +100,7 @@ class FingerprintMapRepositoryTest { @Test fun `fingerprint map with key event action from device and proper device name extra, do not update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -126,7 +124,7 @@ class FingerprintMapRepositoryTest { @Test fun `fingerprint map with key event action from device and blank device name extra, if device for action is disconnected, do not update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -150,7 +148,7 @@ class FingerprintMapRepositoryTest { @Test fun `fingerprint map with key event action from device and blank device name extra, if device for action is connected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -192,7 +190,7 @@ class FingerprintMapRepositoryTest { @Test fun `fingerprint map with key event action from device and no device name extra, if device for action is connected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -234,7 +232,7 @@ class FingerprintMapRepositoryTest { @Test fun `fingerprint map with key event action from device and no device name extra, if device for action is disconnected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, diff --git a/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt b/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt index 3270b5b785..9a390733d9 100644 --- a/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/data/repositories/KeyMapRepositoryTest.kt @@ -11,10 +11,9 @@ import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.util.State import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,9 +44,8 @@ class KeyMapRepositoryTest { ) } - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var repository: RoomKeyMapRepository private lateinit var devicesAdapter: FakeDevicesAdapter @@ -67,7 +65,7 @@ class KeyMapRepositoryTest { repository = RoomKeyMapRepository( mockDao, devicesAdapter, - coroutineScope, + testScope, dispatchers = TestDispatcherProvider(testDispatcher), ) } @@ -77,7 +75,7 @@ class KeyMapRepositoryTest { */ @Test fun `if modifying a huge number of key maps then split job into batches`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val keyMapList = sequence { repeat(991) { @@ -112,7 +110,7 @@ class KeyMapRepositoryTest { @Test fun `key map with key event action from device and proper device name extra, do not update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -136,7 +134,7 @@ class KeyMapRepositoryTest { @Test fun `key map with key event action from device and blank device name extra, if device for action is disconnected, do not update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -160,7 +158,7 @@ class KeyMapRepositoryTest { @Test fun `key map with key event action from device and blank device name extra, if device for action is connected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -195,7 +193,7 @@ class KeyMapRepositoryTest { @Test fun `key map with key event action from device and no device name extra, if device for action is connected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -230,7 +228,7 @@ class KeyMapRepositoryTest { @Test fun `key map with key event action from device and no device name extra, if device for action is disconnected, update action device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = ActionEntity( type = ActionEntity.Type.KEY_EVENT, @@ -254,7 +252,7 @@ class KeyMapRepositoryTest { @Test fun `key map with device name for trigger key, if device for trigger key is connected, do not update trigger key device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val triggerKey = TriggerEntity.KeyEntity( keyCode = 1, @@ -277,7 +275,7 @@ class KeyMapRepositoryTest { @Test fun `key map with device name for trigger key, if device for trigger key is disconnected, do not update trigger key device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val triggerKey = TriggerEntity.KeyEntity( keyCode = 1, @@ -298,7 +296,7 @@ class KeyMapRepositoryTest { @Test fun `key map with no device name for trigger key, if device for trigger key is connected, update trigger key device name`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val triggerKey = TriggerEntity.KeyEntity( keyCode = 1, diff --git a/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt deleted file mode 100644 index 002a4674d5..0000000000 --- a/app/src/test/java/io/github/sds100/keymapper/home/HomeMenuViewModelTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package io.github.sds100.keymapper.home - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import io.github.sds100.keymapper.R -import io.github.sds100.keymapper.util.ui.FakeResourceProvider -import io.github.sds100.keymapper.util.ui.PopupUi -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.withTimeout -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.kotlin.mock - -/** - * Created by sds100 on 29/04/2022. - */ -@ExperimentalCoroutinesApi -class HomeMenuViewModelTest { - - @get:Rule - var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() - private val testCoroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) - - private lateinit var fakeResourceProvider: FakeResourceProvider - private lateinit var viewModel: HomeMenuViewModel - - @Before - fun setUp() { - fakeResourceProvider = FakeResourceProvider() - viewModel = HomeMenuViewModel( - testCoroutineScope, - alertsUseCase = mock(), - pauseMappings = mock(), - showImePicker = mock(), - fakeResourceProvider, - ) - } - - @Test - fun onCreateDocumentActivityNotFound() = runBlockingTest { - // given - fakeResourceProvider.stringResourceMap[R.string.dialog_message_no_app_found_to_create_file] = "message" - fakeResourceProvider.stringResourceMap[R.string.pos_ok] = "ok" - - // when - viewModel.onCreateBackupFileActivityNotFound() - - // then - withTimeout(1000) { - val popupEvent = viewModel.showPopup.first() - assertThat(popupEvent.ui, `is`(PopupUi.Dialog(message = "message", positiveButtonText = "ok"))) - } - } - - @Test - fun onGetContentActivityNotFound() = runBlockingTest { - // given - fakeResourceProvider.stringResourceMap[R.string.dialog_message_no_app_found_to_choose_a_file] = "message" - fakeResourceProvider.stringResourceMap[R.string.pos_ok] = "ok" - - // when - viewModel.onChooseRestoreFileActivityNotFound() - - // then - withTimeout(1000) { - val popupEvent = viewModel.showPopup.first() - assertThat(popupEvent.ui, `is`(PopupUi.Dialog(message = "message", positiveButtonText = "ok"))) - } - } -} diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt index 4e035540c8..1ae33aaaef 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/SimpleMappingControllerTest.kt @@ -9,12 +9,10 @@ import io.github.sds100.keymapper.constraints.DetectConstraintsUseCase import junitparams.JUnitParamsRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import org.junit.After +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,9 +41,8 @@ class SimpleMappingControllerTest { private const val HOLD_DOWN_DURATION = 1000L } - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var controller: SimpleMappingController private lateinit var detectMappingUseCase: DetectMappingUseCase @@ -90,24 +87,19 @@ class SimpleMappingControllerTest { } controller = FakeSimpleMappingController( - coroutineScope, + testScope, detectMappingUseCase, performActionsUseCase, detectConstraintsUseCase, ) } - @After - fun tearDown() { - coroutineScope.cleanupTestCoroutines() - } - /** * #663 */ @Test fun `action with repeat until limit reached shouldn't stop repeating when trigger is detected again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = FakeAction( data = ActionData.InputKeyEvent(1), @@ -131,7 +123,7 @@ class SimpleMappingControllerTest { */ @Test fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = FakeAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -153,7 +145,7 @@ class SimpleMappingControllerTest { */ @Test fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = FakeAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -180,7 +172,7 @@ class SimpleMappingControllerTest { */ @Test fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = FakeAction( data = ActionData.InputKeyEvent(keyCode = 1), diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt index 6c3be50b09..608eda705d 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapTriggerViewModelTest.kt @@ -16,13 +16,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,9 +36,9 @@ import org.mockito.kotlin.mock @RunWith(MockitoJUnitRunner::class) class ConfigKeyMapTriggerViewModelTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private lateinit var viewModel: ConfigKeyMapTriggerViewModel private lateinit var mockConfigKeyMapUseCase: ConfigKeyMapUseCase private lateinit var mockRecordTrigger: RecordTriggerUseCase @@ -69,7 +67,7 @@ class ConfigKeyMapTriggerViewModelTest { fakeResourceProvider = FakeResourceProvider() viewModel = ConfigKeyMapTriggerViewModel( - coroutineScope, + testScope, fakeOnboarding, mockConfigKeyMapUseCase, mockRecordTrigger, @@ -83,17 +81,12 @@ class ConfigKeyMapTriggerViewModelTest { ) } - @After - fun tearDown() { - coroutineScope.cleanupTestCoroutines() - } - /** * issue #602 */ @Test fun `when create back button trigger key then prompt the user to disable screen pinning`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN fakeResourceProvider.stringResourceMap[R.string.dialog_message_screen_pinning_warning] = "bla" 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 547ae25749..0cbfa8b502 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 @@ -30,15 +30,13 @@ import junitparams.naming.TestCaseName import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.createTestCoroutineScope import kotlinx.coroutines.test.currentTime -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -107,9 +105,8 @@ class KeyMapControllerTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun init() { @@ -139,7 +136,7 @@ class KeyMapControllerTest { } } - whenever(detectKeyMapsUseCase.currentTime).thenAnswer { coroutineScope.currentTime } + whenever(detectKeyMapsUseCase.currentTime).thenAnswer { testScope.currentTime } performActionsUseCase = mock { MutableStateFlow(REPEAT_DELAY).apply { @@ -160,21 +157,96 @@ class KeyMapControllerTest { } controller = KeyMapController( - coroutineScope, + testScope, detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, ) } - @After - fun tearDown() { - coroutineScope.cleanupTestCoroutines() - } + /** + * #1271 but with long press trigger instead of double press. + */ + @Test + fun `Trigger short press key map if constraints allow it and a long press key map to the same button is not allowed`() = + runTest(testDispatcher) { + val shortPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) + + val longPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), + ) + val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) + + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = shortPressTrigger, + actionList = listOf(TEST_ACTION), + constraintState = shortPressConstraints, + ), + KeyMap( + 1, + trigger = longPressTrigger, + actionList = listOf(TEST_ACTION_2), + constraintState = doublePressConstraints, + ), + ) + + // Only the short press trigger is allowed. + mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } + + mockTriggerKeyInput(shortPressTrigger.keys.first()) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + } + + /** + * #1271 + */ + @Test + fun `ignore double press key maps overlapping short press key maps if the constraints aren't satisfied`() = + runTest(testDispatcher) { + val shortPressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + val shortPressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOn)) + + val doublePressTrigger = singleKeyTrigger( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), + ) + val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff)) + + keyMapListFlow.value = listOf( + KeyMap( + 0, + trigger = shortPressTrigger, + actionList = listOf(TEST_ACTION), + constraintState = shortPressConstraints, + ), + KeyMap( + 1, + trigger = doublePressTrigger, + actionList = listOf(TEST_ACTION_2), + constraintState = doublePressConstraints, + ), + ) + + // Only the short press trigger is allowed. + mockConstraintSnapshot { constraint -> constraint == Constraint.WifiOn } + + mockTriggerKeyInput(shortPressTrigger.keys.first()) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) + } @Test fun `Don't imitate button if 1 long press trigger is successful and another with a longer delay fails`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val longerTrigger = @@ -250,7 +322,7 @@ class KeyMapControllerTest { */ @Test fun `Long press trigger shouldn't be triggered if the constraints are changed by the actions`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val actionData = ActionData.Flashlight.Toggle(CameraLens.BACK) @@ -303,7 +375,7 @@ class KeyMapControllerTest { */ @Test fun `multiple key maps with the same long press trigger but different long press delays should all work`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val keyMap1 = KeyMap( trigger = KeyMapTrigger( @@ -362,7 +434,7 @@ class KeyMapControllerTest { */ @Test fun `don't consume down and up event if no valid actions to perform`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val actionList = listOf(KeyMapAction(data = ActionData.InputKeyEvent(2))) @@ -387,7 +459,7 @@ class KeyMapControllerTest { * #689 */ @Test - fun `perform all actions once when key map is triggered`() = coroutineScope.runBlockingTest { + fun `perform all actions once when key map is triggered`() = runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -415,7 +487,7 @@ class KeyMapControllerTest { */ @Test fun `action with repeat until limit reached shouldn't stop repeating when trigger is released`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -445,7 +517,7 @@ class KeyMapControllerTest { @Test fun `key map with multiple actions and delay in between, perform all actions even when trigger is released`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -487,7 +559,7 @@ class KeyMapControllerTest { @Test fun `multiple key maps with same trigger, perform both key maps`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -525,7 +597,7 @@ class KeyMapControllerTest { */ @Test fun `when triggering action that repeats until limit reached, then stop repeating when the limit has been reached and not when the trigger is released`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -554,7 +626,7 @@ class KeyMapControllerTest { */ @Test fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when the trigger has been pressed again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -589,7 +661,7 @@ class KeyMapControllerTest { */ @Test fun `when triggering action that repeats until pressed again with repeat limit, then stop repeating when limit reached and trigger hasn't been pressed again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -623,7 +695,7 @@ class KeyMapControllerTest { */ @Test fun `when triggering action that repeats until released with repeat limit, then stop repeating when the trigger has been released`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -653,7 +725,7 @@ class KeyMapControllerTest { */ @Test fun `when triggering action that repeats until released with repeat limit, then stop repeating when the limit has been reached and the action is still being held down`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -682,7 +754,7 @@ class KeyMapControllerTest { * issue #653 */ @Test - fun `overlapping triggers 3`() = coroutineScope.runBlockingTest { + fun `overlapping triggers 3`() = runTest(testDispatcher) { // GIVEN val keyMaps = listOf( KeyMap( @@ -750,7 +822,7 @@ class KeyMapControllerTest { * issue #653 */ @Test - fun `overlapping triggers 2`() = coroutineScope.runBlockingTest { + fun `overlapping triggers 2`() = runTest(testDispatcher) { // GIVEN val keyMaps = listOf( KeyMap( @@ -811,7 +883,7 @@ class KeyMapControllerTest { * issue #653 */ @Test - fun `overlapping triggers 1`() = coroutineScope.runBlockingTest { + fun `overlapping triggers 1`() = runTest(testDispatcher) { // GIVEN val keyMaps = listOf( KeyMap( @@ -908,7 +980,7 @@ class KeyMapControllerTest { */ @Test fun `imitate button presses when a short press trigger with multiple keys fails`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = parallelTrigger( triggerKey(keyCode = 1), @@ -968,7 +1040,7 @@ class KeyMapControllerTest { */ @Test fun `don't imitate button press when a short press trigger is triggered`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = parallelTrigger( triggerKey(keyCode = 1), @@ -998,7 +1070,7 @@ class KeyMapControllerTest { */ @Test fun `don't repeat when trigger is released for an action that has these options when the trigger is held down`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val action = KeyMapAction( data = ActionData.InputKeyEvent(keyCode = 1), @@ -1019,7 +1091,7 @@ class KeyMapControllerTest { mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1) // see if the action repeats - coroutineScope.testScheduler.apply { + testScope.testScheduler.apply { advanceTimeBy(500) runCurrent() } @@ -1042,7 +1114,7 @@ class KeyMapControllerTest { */ @Test fun `don't initialise repeating if repeat when trigger is released after failed long press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1092,7 +1164,7 @@ class KeyMapControllerTest { */ @Test fun `don't initialise repeating if repeat when trigger is released after failed failed double press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1141,7 +1213,7 @@ class KeyMapControllerTest { */ @Test fun `don't initialise repeating if repeat when trigger is released after failed double press and failed long press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1202,7 +1274,7 @@ class KeyMapControllerTest { */ @Test fun `initialise repeating if repeat until pressed again on failed long press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1252,7 +1324,7 @@ class KeyMapControllerTest { */ @Test fun `initialise repeating if repeat until pressed again on failed double press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1307,7 +1379,7 @@ class KeyMapControllerTest { */ @Test fun `initialise repeating if repeat until pressed again on failed double press and failed long press`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val trigger1 = parallelTrigger(triggerKey(keyCode = 1)) val action1 = KeyMapAction( @@ -1368,7 +1440,7 @@ class KeyMapControllerTest { */ @Test fun `short press key and double press same key sequence trigger, double press key, don't perform action`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val trigger = sequenceTrigger( triggerKey(KeyEvent.KEYCODE_A), triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS), @@ -1392,7 +1464,7 @@ class KeyMapControllerTest { * issue #563 */ @Test - fun sendKeyEventActionWhenImitatingButtonPresses() = coroutineScope.runBlockingTest { + fun sendKeyEventActionWhenImitatingButtonPresses() = runTest(testDispatcher) { val trigger = singleKeyTrigger( triggerKey( keyCode = KeyEvent.KEYCODE_META_LEFT, @@ -1553,7 +1625,7 @@ class KeyMapControllerTest { @Test fun `parallel trigger with 2 keys and the 2nd key is another trigger, press 2 key trigger, only the action for 2 key trigger should be performed `() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val twoKeyTrigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_SHIFT_LEFT), @@ -1596,7 +1668,7 @@ class KeyMapControllerTest { @Test fun `trigger for a specific device and trigger for any device, input trigger from a different device, only detect trigger for any device`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val triggerKeyboard = singleKeyTrigger( triggerKey(KeyEvent.KEYCODE_A, FAKE_KEYBOARD_TRIGGER_KEY_DEVICE), @@ -1623,7 +1695,7 @@ class KeyMapControllerTest { @Test fun `trigger for a specific device, input trigger from a different device, do not detect trigger`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val triggerHeadphone = singleKeyTrigger( triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE), @@ -1642,7 +1714,7 @@ class KeyMapControllerTest { @Test fun `long press trigger and action with Hold Down until pressed again flag, input valid long press, hold down until long pressed again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.LONG_PRESS)) @@ -1684,7 +1756,7 @@ class KeyMapControllerTest { */ @Test fun `trigger with modifier key and modifier keycode action, don't include metastate from the trigger modifier key when an unmapped modifier key is pressed`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT)) keyMapListFlow.value = listOf( @@ -1747,7 +1819,7 @@ class KeyMapControllerTest { @Test fun `2x key sequence trigger and 3x key sequence trigger with the last 2 keys being the same, trigger 3x key trigger, ignore the first 2x key trigger`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val firstTrigger = sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, @@ -1784,7 +1856,7 @@ class KeyMapControllerTest { @Test fun `2x key long press parallel trigger with HOME or RECENTS keycode, trigger successfully, don't do normal action`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { /* HOME */ @@ -1835,7 +1907,7 @@ class KeyMapControllerTest { @Test fun shortPressTriggerDoublePressTrigger_holdDown_onlyDetectDoublePressTrigger() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val doublePressTrigger = singleKeyTrigger( @@ -1873,7 +1945,7 @@ class KeyMapControllerTest { @Test fun shortPressTriggerLongPressTrigger_holdDown_onlyDetectLongPressTrigger() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val longPressTrigger = singleKeyTrigger( @@ -1909,7 +1981,7 @@ class KeyMapControllerTest { @Test @Parameters(method = "params_repeatAction") fun parallelTrigger_holdDown_repeatAction10Times(description: String, trigger: KeyMapTrigger) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val action = KeyMapAction( data = ActionData.Volume.Up(showVolumeUi = false), @@ -1957,12 +2029,12 @@ class KeyMapControllerTest { ) @Test - @Parameters(method = "params_dualParallelTrigger_input2ndKey_do notConsumeUp") + @Parameters(method = "params_dualParallelTrigger_input2ndKey_doNotConsumeUp") fun dualParallelTrigger_input2ndKey_doNotConsumeUp( description: String, trigger: KeyMapTrigger, ) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given keyMapListFlow.value = listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) @@ -2007,7 +2079,7 @@ class KeyMapControllerTest { ) @Test - fun dualShortPressParallelTrigger_validInput_consumeUp() = coroutineScope.runBlockingTest { + fun dualShortPressParallelTrigger_validInput_consumeUp() = runTest(testDispatcher) { // given val trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), @@ -2046,7 +2118,7 @@ class KeyMapControllerTest { } @Test - fun dualLongPressParallelTrigger_validInput_consumeUp() = coroutineScope.runBlockingTest { + fun dualLongPressParallelTrigger_validInput_consumeUp() = runTest(testDispatcher) { // given val trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), @@ -2088,7 +2160,7 @@ class KeyMapControllerTest { @Test fun keymappedToLongPressAndDoublePress_invalidLongPress_imitateOnce() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val longPressTrigger = singleKeyTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), @@ -2119,7 +2191,7 @@ class KeyMapControllerTest { @Test fun keymappedToSingleShortPressAndLongPress_validShortPress_onlyPerformActiondoNotImitateKey() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -2149,7 +2221,7 @@ class KeyMapControllerTest { @Test fun keymappedToShortPressAndDoublePress_validShortPress_onlyPerformActionDoNotImitateKey() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val shortPressTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) @@ -2181,7 +2253,7 @@ class KeyMapControllerTest { @Test fun singleKeyTriggerAndShortPressParallelTriggerWithSameInitialKey_validSingleKeyTriggerInput_onlyPerformActiondoNotImitateKey() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // given val singleKeyTrigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val parallelTrigger = parallelTrigger( @@ -2209,7 +2281,7 @@ class KeyMapControllerTest { } @Test - fun longPressSequenceTrigger_invalidLongPress_keyImitated() = coroutineScope.runBlockingTest { + fun longPressSequenceTrigger_invalidLongPress_keyImitated() = runTest(testDispatcher) { val trigger = sequenceTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), triggerKey(KeyEvent.KEYCODE_VOLUME_UP), @@ -2233,7 +2305,7 @@ class KeyMapControllerTest { @Test @Parameters(method = "params_multipleActionsPerformed") fun validInput_multipleActionsPerformed(description: String, trigger: KeyMapTrigger) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { val actionList = listOf(TEST_ACTION, TEST_ACTION_2) // GIVEN keyMapListFlow.value = listOf( @@ -2287,7 +2359,7 @@ class KeyMapControllerTest { @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN keyMapListFlow.value = listOf(keyMap) @@ -2315,7 +2387,7 @@ class KeyMapControllerTest { @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") fun validInput_downConsumed(description: String, keyMap: KeyMap) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN keyMapListFlow.value = listOf(keyMap) @@ -2338,10 +2410,10 @@ class KeyMapControllerTest { } @Test - @Parameters(method = "params_allTriggerKeyCombinationsdo notConsume") + @Parameters(method = "params_allTriggerKeyCombinationsdoNotConsume") @TestCaseName("{0}") fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { keyMapListFlow.value = listOf(keyMap) var consumedCount = 0 @@ -3119,7 +3191,7 @@ class KeyMapControllerTest { @Parameters(method = "params_allTriggerKeyCombinations") @TestCaseName("{0}") fun validInput_actionPerformed(description: String, keyMap: KeyMap) = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN keyMapListFlow.value = listOf(keyMap) @@ -3244,4 +3316,11 @@ class KeyMapControllerTest { isGameController = isGameController, ) } + + private fun mockConstraintSnapshot(isSatisfiedBlock: (constraint: Constraint) -> Boolean) { + val snapshot = object : ConstraintSnapshot { + override fun isSatisfied(constraint: Constraint): Boolean = isSatisfiedBlock(constraint) + } + whenever(detectConstraintsUseCase.getSnapshot()).thenReturn(snapshot) + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt index 7bee039558..429e26b30f 100644 --- a/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/mappings/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt @@ -11,12 +11,10 @@ import junitparams.JUnitParamsRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest -import org.junit.After +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -44,9 +42,8 @@ class TriggerKeyMapFromOtherAppsControllerTest { private const val HOLD_DOWN_DURATION = 1000L } - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var controller: TriggerKeyMapFromOtherAppsController private lateinit var detectKeyMapsUseCase: DetectKeyMapsUseCase @@ -96,42 +93,41 @@ class TriggerKeyMapFromOtherAppsControllerTest { } controller = TriggerKeyMapFromOtherAppsController( - coroutineScope, + testScope, detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, ) } - @After - fun tearDown() { - coroutineScope.cleanupTestCoroutines() - } - /** * #707 */ @Test - fun `Key map with repeat option, don't repeat when triggered if repeat until released`() = coroutineScope.runBlockingTest { - // GIVEN - val action = - KeyMapAction( - data = ActionData.InputKeyEvent(keyCode = 1), - repeat = true, - repeatMode = RepeatMode.TRIGGER_RELEASED, + fun `Key map with repeat option, don't repeat when triggered if repeat until released`() = + runTest(testDispatcher) { + // GIVEN + val action = + KeyMapAction( + data = ActionData.InputKeyEvent(keyCode = 1), + repeat = true, + repeatMode = RepeatMode.TRIGGER_RELEASED, + ) + val keyMap = KeyMap( + actionList = listOf(action), + trigger = KeyMapTrigger(triggerFromOtherApps = true), ) - val keyMap = KeyMap(actionList = listOf(action), trigger = KeyMapTrigger(triggerFromOtherApps = true)) - keyMapListFlow.value = listOf(keyMap) + keyMapListFlow.value = listOf(keyMap) - advanceUntilIdle() + advanceUntilIdle() - // WHEN - controller.onDetected(keyMap.uid) - delay(500) - controller.reset() // stop any repeating that might be happening - advanceUntilIdle() + // WHEN + controller.onDetected(keyMap.uid) + delay(500) + controller.reset() // stop any repeating that might be happening + advanceUntilIdle() - // THEN - verify(performActionsUseCase, times(1)).perform(action.data) - } + // THEN + verify(performActionsUseCase, times(1)).perform(action.data) + } } diff --git a/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt b/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt index 7c1127eab1..84ad35b46e 100644 --- a/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/onboarding/OnboardingUseCaseTest.kt @@ -6,11 +6,10 @@ import io.github.sds100.keymapper.data.repositories.FakePreferenceRepository import io.github.sds100.keymapper.util.VersionHelper import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineExceptionHandler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.createTestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.junit.Before @@ -27,9 +26,8 @@ import org.mockito.kotlin.mock @RunWith(MockitoJUnitRunner::class) class OnboardingUseCaseTest { - private val testDispatcher = TestCoroutineDispatcher() - private val coroutineScope = - createTestCoroutineScope(TestCoroutineDispatcher() + TestCoroutineExceptionHandler() + testDispatcher) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var useCase: OnboardingUseCaseImpl private lateinit var fakePreferences: FakePreferenceRepository @@ -52,7 +50,7 @@ class OnboardingUseCaseTest { */ @Test fun `Only show fingerprint map feature notification for the first update only`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // show it when updating from a version that didn't support it to a version that does // GIVEN fakePreferences.set(Keys.approvedFingerprintFeaturePrompt, false) @@ -100,7 +98,7 @@ class OnboardingUseCaseTest { @Test fun `update to 2_3_0, no bluetooth devices were chosen in settings, do not show notification to choose devices again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN fakePreferences.set( stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker"), @@ -120,7 +118,7 @@ class OnboardingUseCaseTest { @Test fun `update to 2_3_0, bluetooth devices were chosen in settings, show notification to choose devices again`() = - coroutineScope.runBlockingTest { + runTest(testDispatcher) { // GIVEN fakePreferences.set( stringSetPreferencesKey("pref_bluetooth_devices_show_ime_picker"), diff --git a/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt b/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt index e44012ab60..627ff1d7d9 100644 --- a/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/system/intents/ConfigIntentViewModelTest.kt @@ -3,9 +3,14 @@ package io.github.sds100.keymapper.system.intents import android.content.Intent import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.github.sds100.keymapper.util.firstBlocking +import io.github.sds100.keymapper.util.ui.FakeResourceProvider +import io.github.sds100.keymapper.util.ui.MultiChoiceItem +import io.github.sds100.keymapper.util.ui.PopupUi +import io.github.sds100.keymapper.util.ui.ShowPopupEvent +import io.github.sds100.keymapper.util.ui.onUserResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasItem @@ -22,7 +27,7 @@ internal class ConfigIntentViewModelTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() - private val testDispatcher = TestCoroutineDispatcher() + private val testDispatcher = UnconfinedTestDispatcher() private lateinit var fakeResourceProvider: FakeResourceProvider private lateinit var viewModel: ConfigIntentViewModel @@ -53,7 +58,8 @@ internal class ConfigIntentViewModelTest { viewModel.showFlagsDialog() val popupEvent: ShowPopupEvent = viewModel.showPopup.firstBlocking() val multipleChoiceDialog = popupEvent.ui as PopupUi.MultiChoice<*> - val expectedCheckedItem = MultiChoiceItem(Intent.FLAG_ACTIVITY_NEW_TASK, "FLAG_ACTIVITY_NEW_TASK", true) + val expectedCheckedItem = + MultiChoiceItem(Intent.FLAG_ACTIVITY_NEW_TASK, "FLAG_ACTIVITY_NEW_TASK", true) assertThat(multipleChoiceDialog.items, hasItem(expectedCheckedItem)) } diff --git a/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json b/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json index 3964dac5e4..b343a2bbdd 100644 --- a/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json +++ b/app/src/test/resources/backup-manager-test/restore-keymaps-no-db-version.json @@ -14,6 +14,7 @@ "constraintMode": 1, "flags": 0, "id": 0, + "uid": "uid1", "isEnabled": true, "trigger": { "extras": [], @@ -42,6 +43,7 @@ "constraintMode": 1, "flags": 0, "id": 0, + "uid": "uid2", "isEnabled": true, "trigger": { "extras": [], diff --git a/app/version.properties b/app/version.properties index 2d5e7f719b..7860daf1ec 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=2.6.2 -VERSION_CODE=65 +VERSION_NAME=2.6.3 +VERSION_CODE=66 VERSION_NUM=0 \ No newline at end of file diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 659bb4595d..94d9808cc7 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,5 +1 @@ {% extends "base.html" %} - -{% block announce %} -MAINTENANCE NOTICE! Key Mapper is currently in maintenance mode. -{% endblock %} diff --git a/fastlane/metadata/android/uk_UA/full_description.txt b/fastlane/metadata/android/uk_UA/full_description.txt new file mode 100644 index 0000000000..515ccecd74 --- /dev/null +++ b/fastlane/metadata/android/uk_UA/full_description.txt @@ -0,0 +1,38 @@ +What can be remapped? + + * Fingerprint gestures on supported devices. + * Volume buttons. + * Navigation buttons. + * Bluetooth/wired keyboards. + * Buttons on other connected devices should also work. + +ONLY HARDWARE buttons can be remapped. +There is NO GUARANTEE any of these buttons will work and this app is NOT designed to control games. Your device's OEM/vendor can prevent them from being remapped. + +You can combine multiple keys from a specific device or any device to form a "trigger". Each trigger can have multiple actions. The keys can be set to be pressed at the same time or one after another in a sequence. Keys can be remapped when they are short pressed, long pressed or double pressed. A keymap can have a set of "constraints" so it only has an effect in certain situations. + +What can’t be remapped? + * Power button + * Bixby button + * Mouse buttons + * Dpad, thumb sticks or triggers on game controllers + +Your key maps don't work if the screen is OFF. This is a limitation in Android. There is nothing the dev can do. + +What can I remap my keys to do? +Some actions will only work on rooted devices and specific Android versions. + +There are too many features to list here so check out the full list here: https://docs.keymapper.club/user-guide/actions + +Permissions +You don't have to grant all the permissions for the app to work. The app will tell you if a permission needs to be granted for a feature to work. + + * Accessibility Service: Basic requirement for remapping to work. It is needed so the app can listen to and block keyevents. + * Device Admin: To turn the screen off when using the action to turn off the screen. + * Modify System Settings: To change the brightness and rotation settings. + * Camera: To control the flashlight. + + On some devices, enabling the accessibility service will disable "enhanced data encryption". + +Discord: www.keymapper.club +Website: docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/uk_UA/short_description.txt b/fastlane/metadata/android/uk_UA/short_description.txt new file mode 100644 index 0000000000..4de8bd060b --- /dev/null +++ b/fastlane/metadata/android/uk_UA/short_description.txt @@ -0,0 +1 @@ +Unleash your keys! Open source! \ No newline at end of file diff --git a/fastlane/metadata/android/uk_UA/title.txt b/fastlane/metadata/android/uk_UA/title.txt new file mode 100644 index 0000000000..19f819ebd7 --- /dev/null +++ b/fastlane/metadata/android/uk_UA/title.txt @@ -0,0 +1 @@ +Key Mapper \ No newline at end of file diff --git a/fastlane/metadata/android/vi_VN/full_description.txt b/fastlane/metadata/android/vi_VN/full_description.txt new file mode 100644 index 0000000000..459f5b0cb3 --- /dev/null +++ b/fastlane/metadata/android/vi_VN/full_description.txt @@ -0,0 +1,38 @@ +Những gì có thể được ánh xạ lại? + + * Cử chỉ vân tay trên các thiết bị được hỗ trợ. + * Nút âm lượng. + * Các nút điều hướng. + * Bàn phím Bluetooth/có dây. + * Các nút trên các thiết bị được kết nối khác cũng sẽ hoạt động. + +CHỈ có thể ánh xạ lại các nút PHẦN CỨNG. +KHÔNG CÓ ĐẢM BẢO bất kỳ nút nào trong số này sẽ hoạt động và ứng dụng này KHÔNG được thiết kế để điều khiển trò chơi. OEM/nhà cung cấp thiết bị của bạn có thể ngăn không cho chúng được ánh xạ lại. + +Bạn có thể kết hợp nhiều phím từ một thiết bị cụ thể hoặc bất kỳ thiết bị nào để tạo thành một "trình kích hoạt". Mỗi trigger có thể có nhiều hành động. Các phím có thể được cài đặt để nhấn cùng lúc hoặc lần lượt theo trình tự. Các phím có thể được ánh xạ lại khi chúng được nhấn nhanh, nhấn lâu hoặc nhấn đúp. Sơ đồ bàn phím có thể có một tập hợp các "ràng buộc" nên nó chỉ có tác dụng trong một số trường hợp nhất định. + +Những gì không thể được ánh xạ lại? + * Nút nguồn + * Nút Bixby + * Nút chuột + * Dpad, gậy ngón tay cái hoặc trình kích hoạt trên bộ điều khiển trò chơi + +Bản đồ chính của bạn không hoạt động nếu màn hình TẮT. Đây là một hạn chế trong Android. Dev không thể làm gì được. + +Tôi có thể sắp xếp lại chìa khóa của mình để làm gì? +Một số hành động sẽ chỉ hoạt động trên các thiết bị đã root và các phiên bản Android cụ thể. + +Có quá nhiều tính năng để liệt kê ở đây, vì vậy hãy xem danh sách đầy đủ tại đây: https://docs.keymapper.club/user-guide/actions + +Quyền +Bạn không cần phải cấp tất cả các quyền để ứng dụng hoạt động. Ứng dụng sẽ cho bạn biết liệu có cần cấp quyền để một tính năng hoạt động hay không. + + * Dịch vụ trợ năng: Yêu cầu cơ bản để ánh xạ lại hoạt động. Nó cần thiết để ứng dụng có thể nghe và chặn các sự kiện quan trọng. + * Quản trị thiết bị: Để tắt màn hình khi sử dụng thao tác tắt màn hình. + * Sửa đổi cài đặt hệ thống: Để thay đổi cài đặt độ sáng và xoay. + * Camera: Để điều khiển đèn pin. + + Trên một số thiết bị, việc bật dịch vụ trợ năng sẽ vô hiệu hóa "mã hóa dữ liệu nâng cao". + +Discord: www.keymapper.club +Website: docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/vi_VN/short_description.txt b/fastlane/metadata/android/vi_VN/short_description.txt new file mode 100644 index 0000000000..725018974c --- /dev/null +++ b/fastlane/metadata/android/vi_VN/short_description.txt @@ -0,0 +1 @@ +Giải phóng chìa khóa của bạn! Nguồn mở! \ No newline at end of file diff --git a/fastlane/metadata/android/vi_VN/title.txt b/fastlane/metadata/android/vi_VN/title.txt new file mode 100644 index 0000000000..19f819ebd7 --- /dev/null +++ b/fastlane/metadata/android/vi_VN/title.txt @@ -0,0 +1 @@ +Key Mapper \ No newline at end of file