diff --git a/CHANGELOG.md b/CHANGELOG.md index 080df16191..9fb8048a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [3.0.1](https://github.com/sds100/KeyMapper/releases/tag/v3.0.1) + +#### 28 April 2025 + +## Added + +- #1652 Bring back the menu button to show input method picker. +- #1657 Turn on repeat by default for volume actions. + +## Changed + +- #1654 The Key Mapper keyboard is now required again for Text actions because the accessibility service API does not work in all situations. +- #1653 Hide the export/import menu buttons in groups. +- #1553 Hide double press option for side key and fingerprint gesture triggers because it is misleading. Double activations can be done with sequence triggers instead. +- #1669 Change quick settings tile text. + +## Bug fixes + +- Inputting key events with Shizuku does not crash the app if a Key Mapper keyboard is being used at the same time. And latency when inputting key events has been improved in some apps. +- #1646 disabling Bluetooth clears the list of connected devices. +- #1655 do not crash when restoring key map groups. +- #1649 show purchase verification failed error if no network connection. +- #1648 caching purchases works so you can use floating buttons and assistant trigger without an internet connection. +- #1658 floating buttons appear in the wrong place in portrait if saved in landscape. +- #1659 Use trigger does not work if the screen orientation changes when re-entering the app. +- #1668 Crashes when floating menu does not fit in the display height. +- #1667 Hold down mode UI is missing from 2.8. + ## [3.0.0](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0) _See the changes from previous 3.0 Beta releases._ diff --git a/app/build.gradle b/app/build.gradle index 8d92d65056..00b2fdc6af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,7 +221,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.viewpager2:viewpager2:1.1.0" - implementation "androidx.datastore:datastore-preferences:1.1.4" + implementation "androidx.datastore:datastore-preferences:1.2.0-alpha01" implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.activity:activity-compose:1.10.1" implementation "androidx.navigation:navigation-compose:2.8.9" diff --git a/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt b/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt index a5dfd67817..88a2d9a5ee 100644 --- a/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt +++ b/app/src/free/java/io/github/sds100/keymapper/purchasing/PurchasingManagerImpl.kt @@ -14,7 +14,8 @@ class PurchasingManagerImpl( private val coroutineScope: CoroutineScope, ) : PurchasingManager { override val onCompleteProductPurchase: MutableSharedFlow = MutableSharedFlow() - override val purchases: Flow>> = MutableStateFlow(State.Data(emptySet())) + override val purchases: Flow>>> = + MutableStateFlow(State.Data(Error.PurchasingNotImplemented)) override suspend fun launchPurchasingFlow(product: ProductId): Result { return Error.PurchasingNotImplemented @@ -27,4 +28,6 @@ class PurchasingManagerImpl( override suspend fun isPurchased(product: ProductId): Result { return Error.PurchasingNotImplemented } + + override fun refresh() {} } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78e07f48d0..c19777221d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -254,7 +254,7 @@ android:name=".system.tiles.ToggleMappingsTile" android:exported="true" android:icon="@drawable/ic_tile_pause" - android:label="@string/tile_pause" + android:label="@string/tile_pause_title" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" tools:targetApi="24"> diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index e7303018d8..7c29bd65d8 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -1,13 +1,13 @@ Key Mapper 3.0 is here! 🎉 -🫧 This release introduces Floating Buttons: you can create custom on-screen buttons to trigger key maps. +🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. 🗂️ Grouping key maps into folders with shared constraints. -🔦 You can now change the flashlight brightness. Tip: use the constraint for when the flashlight is showing to remap your volume buttons to change the brightness. +🔦 Change the flashlight brightness. Tip: use the constraint for when the flashlight is showing to remap your volume buttons to change the brightness. 🛜 Send HTTP requests with a new action. -❤️ There are also tonnes of improvements to make your key mapping experience more enjoyable. +❤️ Many improvements to make your key mapping experience more enjoyable. See all the changes at http://changelog.keymapper.club. \ No newline at end of file diff --git a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt index 22e395491a..caee6e48b0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/UseCases.kt +++ b/app/src/main/java/io/github/sds100/keymapper/UseCases.kt @@ -142,7 +142,7 @@ object UseCases { ServiceLocator.intentAdapter(ctx), getActionError(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), - ShizukuInputEventInjector(), + ShizukuInputEventInjector(coroutineScope = ServiceLocator.appCoroutineScope(ctx)), ServiceLocator.packageManagerAdapter(ctx), ServiceLocator.appShortcutAdapter(ctx), ServiceLocator.popupMessageAdapter(ctx), @@ -179,11 +179,12 @@ object UseCases { ServiceLocator.audioAdapter(ctx), keyMapperImeMessenger(ctx, keyEventRelayService), service, - ShizukuInputEventInjector(), + ShizukuInputEventInjector(ServiceLocator.appCoroutineScope(ctx)), ServiceLocator.popupMessageAdapter(ctx), ServiceLocator.permissionAdapter(ctx), ServiceLocator.resourceProvider(ctx), ServiceLocator.vibratorAdapter(ctx), + ServiceLocator.appCoroutineScope(ctx), ) fun rerouteKeyEvents(ctx: Context, keyEventRelayService: KeyEventRelayServiceWrapper) = RerouteKeyEventsUseCaseImpl( diff --git a/app/src/main/java/io/github/sds100/keymapper/about/AboutFragment.kt b/app/src/main/java/io/github/sds100/keymapper/about/AboutFragment.kt index deb0c6fa00..7c994503ae 100644 --- a/app/src/main/java/io/github/sds100/keymapper/about/AboutFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/about/AboutFragment.kt @@ -57,7 +57,7 @@ class AboutFragment : Fragment() { onBackPressed() } - version = Constants.VERSION + version = "${Constants.VERSION} ${Constants.VERSION_CODE}" } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionOptionsBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionOptionsBottomSheet.kt index bfc4bea515..4a8de35bf8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionOptionsBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionOptionsBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -44,6 +45,7 @@ import io.github.sds100.keymapper.util.ui.SliderStepSizes import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.RadioButtonText import io.github.sds100.keymapper.util.ui.compose.SliderOptionText +import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -63,6 +65,7 @@ fun ActionOptionsBottomSheet( dragHandle = {}, ) { val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current val helpUrl = stringResource(R.string.url_keymap_action_options_guide) val scope = rememberCoroutineScope() @@ -80,7 +83,7 @@ fun ActionOptionsBottomSheet( modifier = Modifier .align(Alignment.TopEnd) .padding(horizontal = 8.dp), - onClick = { uriHandler.openUri(helpUrl) }, + onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, ) { Icon( imageVector = Icons.AutoMirrored.Rounded.HelpOutline, @@ -261,7 +264,36 @@ fun ActionOptionsBottomSheet( ) } - Spacer(Modifier.height(8.dp)) + if (state.showHoldDownMode) { + Spacer(Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.hold_down_until_trigger_is_dot_dot_dot), + style = MaterialTheme.typography.titleSmall, + ) + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButtonText( + isSelected = state.holdDownMode == HoldDownMode.TRIGGER_RELEASED, + text = stringResource(R.string.stop_holding_down_when_trigger_released), + onSelected = { callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_RELEASED) }, + ) + + RadioButtonText( + isSelected = state.holdDownMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, + text = stringResource(R.string.stop_holding_down_trigger_pressed_again), + onSelected = { callback.onSelectHoldDownMode(HoldDownMode.TRIGGER_PRESSED_AGAIN) }, + ) + + Spacer(Modifier.width(8.dp)) + } + } if (state.showHoldDown) { Spacer(Modifier.height(8.dp)) @@ -352,6 +384,7 @@ interface ActionOptionsBottomSheetCallback { fun onRepeatDelayChanged(delay: Int) = run { } fun onHoldDownCheckedChange(checked: Boolean) = run { } fun onHoldDownDurationChanged(duration: Int) = run { } + fun onSelectHoldDownMode(holdDownMode: HoldDownMode) = run { } fun onDelayBeforeNextActionChanged(delay: Int) = run { } fun onMultiplierChanged(multiplier: Int) = run { } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index f1f755074e..e3b696fc8c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -781,8 +781,7 @@ fun ActionData.canBeHeldDown(): Boolean = when (this) { fun ActionData.canUseImeToPerform(): Boolean = when (this) { is ActionData.InputKeyEvent -> !useShell - // Android 13+ can use the accessibility service to input text. - is ActionData.Text -> Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + is ActionData.Text -> true is ActionData.MoveCursorToEnd -> true else -> false } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt index 2d5ec2105a..f501900c70 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ConfigActionsViewModel.kt @@ -219,6 +219,15 @@ class ConfigActionsViewModel( actionOptionsUid.value?.let { uid -> config.setActionHoldDownDuration(uid, duration) } } + override fun onSelectHoldDownMode(holdDownMode: HoldDownMode) { + actionOptionsUid.value?.let { uid -> + config.setActionStopHoldingDownWhenTriggerPressedAgain( + uid, + holdDownMode == HoldDownMode.TRIGGER_PRESSED_AGAIN, + ) + } + } + override fun onDelayBeforeNextActionChanged(delay: Int) { actionOptionsUid.value?.let { uid -> config.setDelayBeforeNextAction(uid, delay) } } @@ -230,7 +239,10 @@ class ConfigActionsViewModel( override fun onSelectRepeatMode(repeatMode: RepeatMode) { actionOptionsUid.value?.let { uid -> when (repeatMode) { - RepeatMode.TRIGGER_RELEASED -> config.setActionStopRepeatingWhenTriggerReleased(uid) + RepeatMode.TRIGGER_RELEASED -> config.setActionStopRepeatingWhenTriggerReleased( + uid, + ) + RepeatMode.LIMIT_REACHED -> config.setActionStopRepeatingWhenLimitReached(uid) RepeatMode.TRIGGER_PRESSED_AGAIN -> config.setActionStopRepeatingWhenTriggerPressedAgain( uid, @@ -416,7 +428,8 @@ class ConfigActionsViewModel( return ConfigActionsState.Empty(shortcuts = shortcuts) } - val actions = createListItems(keyMap, showDeviceDescriptors, errorSnapshot, shortcuts.size) + val actions = + createListItems(keyMap, showDeviceDescriptors, errorSnapshot, shortcuts.size) return ConfigActionsState.Loaded( actions = actions, @@ -542,7 +555,9 @@ class ConfigActionsViewModel( holdDownDuration = action.holdDownDuration ?: defaultHoldDownDuration, defaultHoldDownDuration = defaultHoldDownDuration, - showHoldDownMode = keyMap.isStopHoldingDownActionWhenTriggerPressedAgainAllowed(action), + showHoldDownMode = keyMap.isStopHoldingDownActionWhenTriggerPressedAgainAllowed( + action, + ), holdDownMode = if (action.stopHoldDownWhenTriggerPressedAgain) { HoldDownMode.TRIGGER_PRESSED_AGAIN } else { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 53c7efe637..9734435728 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -67,7 +67,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import splitties.bitflags.withFlag import timber.log.Timber @@ -114,6 +113,7 @@ class PerformActionsUseCaseImpl( accessibilityService, shizukuInputEventInjector, permissionAdapter, + coroutineScope, ) } @@ -124,7 +124,7 @@ class PerformActionsUseCaseImpl( permissionAdapter.isGrantedFlow(Permission.SHIZUKU) .stateIn(coroutineScope, SharingStarted.Eagerly, false) - override fun perform( + override suspend fun perform( action: ActionData, inputEventType: InputEventType, keyMetaState: Int, @@ -132,7 +132,7 @@ class PerformActionsUseCaseImpl( /** * Is null if the action is being performed asynchronously */ - val result: Result<*>? + val result: Result<*> when (action) { is ActionData.App -> { @@ -254,20 +254,15 @@ class PerformActionsUseCaseImpl( } is ActionData.SwitchKeyboard -> { - coroutineScope.launch { - inputMethodAdapter - .chooseImeWithoutUserInput(action.imeId) - .onSuccess { - val message = resourceProvider.getString( - R.string.toast_chose_keyboard, - it.label, - ) - popupMessageAdapter.showPopupMessage(message) - } - .showErrorMessageOnFail() - } - - result = null + result = inputMethodAdapter + .chooseImeWithoutUserInput(action.imeId) + .onSuccess { + val message = resourceProvider.getString( + R.string.toast_chose_keyboard, + it.label, + ) + popupMessageAdapter.showPopupMessage(message) + } } is ActionData.Volume.Down -> { @@ -333,11 +328,7 @@ class PerformActionsUseCaseImpl( } is ActionData.Text -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - accessibilityService.inputText(action.text) - } else { - imeInputEventInjector.inputText(action.text) - } + imeInputEventInjector.inputText(action.text) result = Success(Unit) } @@ -578,14 +569,10 @@ class PerformActionsUseCaseImpl( } is ActionData.GoLastApp -> { - coroutineScope.launch { - accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) - delay(100) + accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) + delay(100) + result = accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) - .showErrorMessageOnFail() - } - - result = null } is ActionData.OpenMenu -> { @@ -711,11 +698,11 @@ class PerformActionsUseCaseImpl( is ActionData.Screenshot -> { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - coroutineScope.launch { - val picturesFolder = fileAdapter.getPicturesFolder() - val screenshotsFolder = "$picturesFolder/Screenshots" - val fileDate = FileUtils.createFileDate() + val picturesFolder = fileAdapter.getPicturesFolder() + val screenshotsFolder = "$picturesFolder/Screenshots" + val fileDate = FileUtils.createFileDate() + result = suAdapter.execute("mkdir -p $screenshotsFolder; screencap -p $screenshotsFolder/Screenshot_$fileDate.png") .onSuccess { // Wait 3 seconds so the message isn't shown in the screenshot. @@ -726,9 +713,7 @@ class PerformActionsUseCaseImpl( R.string.toast_screenshot_taken, ), ) - }.showErrorMessageOnFail() - } - result = null + } } else { result = accessibilityService.doGlobalAction(AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT) @@ -781,19 +766,11 @@ class PerformActionsUseCaseImpl( } ActionData.DismissAllNotifications -> { - coroutineScope.launch { - notificationReceiverAdapter.send(ServiceEvent.DismissAllNotifications) - } - - result = null + result = notificationReceiverAdapter.send(ServiceEvent.DismissAllNotifications) } ActionData.DismissLastNotification -> { - coroutineScope.launch { - notificationReceiverAdapter.send(ServiceEvent.DismissLastNotification) - } - - result = null + result = notificationReceiverAdapter.send(ServiceEvent.DismissLastNotification) } ActionData.AnswerCall -> { @@ -815,27 +792,23 @@ class PerformActionsUseCaseImpl( } is ActionData.HttpRequest -> { - coroutineScope.launch { - networkAdapter.sendHttpRequest( - method = action.method, - url = action.url, - body = action.body, - authorizationHeader = action.authorizationHeader, - ).showErrorMessageOnFail() - } - - result = null + result = networkAdapter.sendHttpRequest( + method = action.method, + url = action.url, + body = action.body, + authorizationHeader = action.authorizationHeader, + ) } } when (result) { - null, is Success -> Timber.d("Performed action $action, input event type: $inputEventType, key meta state: $keyMetaState") + is Success -> Timber.d("Performed action $action, input event type: $inputEventType, key meta state: $keyMetaState") is Error -> Timber.d( "Failed to perform action $action, reason: ${result.getFullMessage(resourceProvider)}, action: $action, input event type: $inputEventType, key meta state: $keyMetaState", ) } - result?.showErrorMessageOnFail() + result.showErrorMessageOnFail() } override fun getErrorSnapshot(): ActionErrorSnapshot { @@ -925,7 +898,7 @@ interface PerformActionsUseCase { val defaultRepeatDelay: Flow val defaultRepeatRate: Flow - fun perform( + suspend fun perform( action: ActionData, inputEventType: InputEventType = InputEventType.DOWN_UP, keyMetaState: Int = 0, diff --git a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt index 7b9abecc16..659c66b79e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt @@ -70,6 +70,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException import java.io.InputStream +import java.util.LinkedList import java.util.UUID /** @@ -410,7 +411,12 @@ class BackupManagerImpl( val soundFiles = soundDir.listFiles() ?: emptyList() // null if dir doesn't exist return@then parseBackupContent(inputStream).then { backupContent -> - restore(restoreType, backupContent, soundFiles) + restore( + restoreType, + backupContent, + soundFiles, + currentTime = System.currentTimeMillis(), + ) } } } @@ -441,25 +447,45 @@ class BackupManagerImpl( } } - private suspend fun restore( + suspend fun restore( restoreType: RestoreType, backupContent: BackupContent, soundFiles: List, + currentTime: Long, ): Result<*> { try { // MUST come before restoring key maps so it is possible to // validate that each key map's group exists in the repository. if (backupContent.groups != null) { - val groupUids = backupContent.groups.map { it.uid }.toMutableSet() - val existingGroupUids = groupRepository.getAllGroups().first() .map { it.uid } .toSet() - .also { groupUids.addAll(it) } - val currentTime = System.currentTimeMillis() + val groupUids = backupContent.groups.map { it.uid }.toMutableSet() + + groupUids.addAll(existingGroupUids) + + // Group parents must be restored first so an SqliteConstraintException + // is not thrown when restoring a child group. + val groupsToRestoreMap = backupContent.groups.associateBy { it.uid }.toMutableMap() + val groupRestoreQueue = LinkedList() + // Order the groups into a queue such that a parent is always before a child. for (group in backupContent.groups) { + if (groupsToRestoreMap.containsKey(group.uid)) { + groupRestoreQueue.addFirst(group) + } + + var parent = groupsToRestoreMap[group.parentUid] + + while (parent != null) { + groupRestoreQueue.addFirst(parent) + groupsToRestoreMap.remove(parent.uid) + parent = groupsToRestoreMap[parent.parentUid] + } + } + + for (group in groupRestoreQueue) { // Set the last opened date to now so that the imported group // shows as the most recent. var modifiedGroup = group.copy(lastOpenedDate = currentTime) diff --git a/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt b/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt index 9c51de7b05..b4fba3ba06 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt @@ -1,11 +1,9 @@ package io.github.sds100.keymapper.data.repositories import android.content.Context -import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import io.github.sds100.keymapper.Constants import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -17,21 +15,9 @@ class SettingsPreferenceRepository( private val coroutineScope: CoroutineScope, ) : PreferenceRepository { - companion object { - private const val DEFAULT_SHARED_PREFS_NAME = "${Constants.PACKAGE_NAME}_preferences" - } - private val ctx = context.applicationContext - private val sharedPreferencesMigration = SharedPreferencesMigration( - ctx, - DEFAULT_SHARED_PREFS_NAME, - ) - - private val Context.dataStore by preferencesDataStore( - name = "preferences", - produceMigrations = { listOf(sharedPreferencesMigration) }, - ) + private val Context.dataStore by preferencesDataStore(name = "preferences") private val dataStore = ctx.dataStore diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt index 0f955052e4..dc46702f69 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeKeyMapListScreen.kt @@ -12,8 +12,13 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward @@ -68,6 +73,7 @@ import io.github.sds100.keymapper.util.drawable import io.github.sds100.keymapper.util.ui.compose.CollapsableFloatingActionButton import io.github.sds100.keymapper.util.ui.compose.ComposeChipModel import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.util.ui.compose.openUriSafe @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -140,6 +146,7 @@ fun HomeKeyMapListScreen( val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current val helpUrl = stringResource(R.string.url_quick_start_guide) var keyMapListBottomPadding by remember { mutableStateOf(100.dp) } @@ -154,7 +161,9 @@ fun HomeKeyMapListScreen( exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it }), ) { CollapsableFloatingActionButton( - modifier = Modifier.padding(bottom = fabBottomPadding), + modifier = Modifier + .padding(bottom = fabBottomPadding) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.End)), onClick = viewModel::onNewKeyMapClick, showText = viewModel.showFabText, text = stringResource(R.string.home_fab_new_key_map), @@ -183,9 +192,10 @@ fun HomeKeyMapListScreen( onSettingsClick = onSettingsClick, onAboutClick = onAboutClick, onSortClick = { viewModel.showSortBottomSheet = true }, - onHelpClick = { uriHandler.openUri(helpUrl) }, + onHelpClick = { uriHandler.openUriSafe(ctx, helpUrl) }, onExportClick = viewModel::onExportClick, onImportClick = { importFileLauncher.launch(FileUtils.MIME_TYPE_ALL) }, + onInputMethodPickerClick = viewModel::showInputMethodPicker, onTogglePausedClick = viewModel::onTogglePausedClick, onFixWarningClick = viewModel::onFixWarningClick, onBackClick = { diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt index d1b08873db..0381317498 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -106,8 +105,7 @@ private fun HomeScreen( Column( modifier // Only take the horizontal because the status bar is the same color as the app bar - .windowInsetsPadding(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)) - .navigationBarsPadding(), + .windowInsetsPadding(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), ) { Box(contentAlignment = Alignment.BottomCenter) { NavHost( diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt index 9b5212b89a..636a9dea5a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt @@ -19,6 +19,7 @@ import io.github.sds100.keymapper.mappings.keymaps.ListKeyMapsUseCase import io.github.sds100.keymapper.mappings.keymaps.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.onboarding.OnboardingUseCase import io.github.sds100.keymapper.sorting.SortKeyMapsUseCase +import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.NavigationViewModel import io.github.sds100.keymapper.util.ui.NavigationViewModelImpl @@ -48,6 +49,7 @@ class HomeViewModel( private val setupGuiKeyboard: SetupGuiKeyboardUseCase, private val sortKeyMaps: SortKeyMapsUseCase, private val listFloatingLayouts: ListFloatingLayoutsUseCase, + private val showInputMethodPickerUseCase: ShowInputMethodPickerUseCase, ) : ViewModel(), ResourceProvider by resourceProvider, PopupViewModel by PopupViewModelImpl(), @@ -78,6 +80,7 @@ class HomeViewModel( showAlertsUseCase, pauseKeyMaps, backupRestore, + showInputMethodPickerUseCase, ) } @@ -186,6 +189,7 @@ class HomeViewModel( private val setupGuiKeyboard: SetupGuiKeyboardUseCase, private val sortKeyMaps: SortKeyMapsUseCase, private val listFloatingLayouts: ListFloatingLayoutsUseCase, + private val showInputMethodPickerUseCase: ShowInputMethodPickerUseCase, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T = HomeViewModel( @@ -198,6 +202,7 @@ class HomeViewModel( setupGuiKeyboard, sortKeyMaps, listFloatingLayouts, + showInputMethodPickerUseCase, ) as T } } diff --git a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt index 8ea94d7eee..2990b29b39 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt @@ -42,6 +42,7 @@ import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.IosShare +import androidx.compose.material.icons.rounded.Keyboard import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.PauseCircleOutline import androidx.compose.material.icons.rounded.PlayCircleOutline @@ -122,6 +123,7 @@ fun KeyMapListAppBar( onFixWarningClick: (String) -> Unit = {}, onExportClick: () -> Unit = {}, onImportClick: () -> Unit = {}, + onInputMethodPickerClick: () -> Unit = {}, onBackClick: () -> Unit = {}, onSelectAllClick: () -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), @@ -158,13 +160,41 @@ fun KeyMapListAppBar( } }, actions = { + var expandedDropdown by rememberSaveable { mutableStateOf(false) } + AppBarActions( onHelpClick, - onSortClick, - onSettingsClick, - onAboutClick, - onExportClick, - onImportClick, + onMenuClick = { expandedDropdown = true }, + dropdownMenuContent = { + RootGroupDropdownMenu( + expanded = expandedDropdown, + onSortClick = { + expandedDropdown = false + onSortClick() + }, + onSettingsClick = { + expandedDropdown = false + onSettingsClick() + }, + onAboutClick = { + expandedDropdown = false + onAboutClick() + }, + onExportClick = { + expandedDropdown = false + onExportClick() + }, + onImportClick = { + expandedDropdown = false + onImportClick() + }, + onInputMethodPickerClick = { + expandedDropdown = false + onInputMethodPickerClick() + }, + onDismissRequest = { expandedDropdown = false }, + ) + }, ) }, ) @@ -252,17 +282,32 @@ fun KeyMapListAppBar( onFixConstraintClick = onFixConstraintClick, actions = { AnimatedVisibility(!state.isEditingGroupName) { + var expandedDropdown by rememberSaveable { mutableStateOf(false) } + AppBarActions( onHelpClick, - onSortClick, - onSettingsClick, - onAboutClick, - onExportClick, - onImportClick, - showDeleteGroup = true, - showSort = true, - onDeleteGroupClick = { - showDeleteGroupDialog = true + onMenuClick = { expandedDropdown = true }, + dropdownMenuContent = { + ChildGroupDropdownMenu( + expanded = expandedDropdown, + onSortClick = { + expandedDropdown = false + onSortClick() + }, + onSettingsClick = { + expandedDropdown = false + onSettingsClick() + }, + onAboutClick = { + expandedDropdown = false + onAboutClick() + }, + onDismissRequest = { expandedDropdown = false }, + onDeleteGroupClick = { + expandedDropdown = false + showDeleteGroupDialog = true + }, + ) }, ) } @@ -532,17 +577,9 @@ private fun SelectingAppBar( @Composable private fun AppBarActions( onHelpClick: () -> Unit, - onSortClick: () -> Unit, - onSettingsClick: () -> Unit, - onAboutClick: () -> Unit, - onExportClick: () -> Unit, - onImportClick: () -> Unit, - showDeleteGroup: Boolean = false, - showSort: Boolean = false, - onDeleteGroupClick: () -> Unit = {}, + onMenuClick: () -> Unit = {}, + dropdownMenuContent: @Composable () -> Unit, ) { - var expandedDropdown by rememberSaveable { mutableStateOf(false) } - Row { IconButton(onClick = onHelpClick) { Icon( @@ -551,43 +588,14 @@ private fun AppBarActions( ) } - IconButton(onClick = { expandedDropdown = true }) { + IconButton(onClick = onMenuClick) { Icon( Icons.Rounded.MoreVert, contentDescription = stringResource(R.string.home_app_bar_more), ) } - AppBarDropdownMenu( - expanded = expandedDropdown, - onSortClick = { - expandedDropdown = false - onSortClick() - }, - onSettingsClick = { - expandedDropdown = false - onSettingsClick() - }, - onAboutClick = { - expandedDropdown = false - onAboutClick() - }, - onExportClick = { - expandedDropdown = false - onExportClick() - }, - onImportClick = { - expandedDropdown = false - onImportClick() - }, - onDismissRequest = { expandedDropdown = false }, - showDeleteGroup = showDeleteGroup, - showSort = showSort, - onDeleteGroupClick = { - expandedDropdown = false - onDeleteGroupClick() - }, - ) + dropdownMenuContent() } } @@ -818,38 +826,20 @@ private fun selectedTextTransition( } @Composable -private fun AppBarDropdownMenu( +private fun RootGroupDropdownMenu( expanded: Boolean, onSortClick: () -> Unit = {}, onSettingsClick: () -> Unit = {}, onAboutClick: () -> Unit = {}, onExportClick: () -> Unit = {}, onImportClick: () -> Unit = {}, + onInputMethodPickerClick: () -> Unit = {}, onDismissRequest: () -> Unit = {}, - showDeleteGroup: Boolean = false, - showSort: Boolean = false, - onDeleteGroupClick: () -> Unit = {}, ) { DropdownMenu( expanded = expanded, onDismissRequest = onDismissRequest, ) { - if (showDeleteGroup) { - DropdownMenuItem( - leadingIcon = { Icon(Icons.Rounded.Delete, contentDescription = null) }, - text = { Text(stringResource(R.string.home_menu_delete_group)) }, - onClick = onDeleteGroupClick, - ) - } - - if (showSort) { - DropdownMenuItem( - leadingIcon = { Icon(Icons.AutoMirrored.Rounded.Sort, contentDescription = null) }, - text = { Text(stringResource(R.string.home_app_bar_sort)) }, - onClick = onSortClick, - ) - } - DropdownMenuItem( leadingIcon = { Icon(Icons.Rounded.Settings, contentDescription = null) }, text = { Text(stringResource(R.string.home_menu_settings)) }, @@ -865,6 +855,47 @@ private fun AppBarDropdownMenu( text = { Text(stringResource(R.string.home_menu_import)) }, onClick = onImportClick, ) + DropdownMenuItem( + leadingIcon = { Icon(Icons.Rounded.Keyboard, contentDescription = null) }, + text = { Text(stringResource(R.string.home_menu_input_method_picker)) }, + onClick = onInputMethodPickerClick, + ) + DropdownMenuItem( + leadingIcon = { Icon(Icons.Rounded.Info, contentDescription = null) }, + text = { Text(stringResource(R.string.home_menu_about)) }, + onClick = onAboutClick, + ) + } +} + +@Composable +private fun ChildGroupDropdownMenu( + expanded: Boolean, + onSortClick: () -> Unit = {}, + onSettingsClick: () -> Unit = {}, + onAboutClick: () -> Unit = {}, + onDismissRequest: () -> Unit = {}, + onDeleteGroupClick: () -> Unit = {}, +) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + ) { + DropdownMenuItem( + leadingIcon = { Icon(Icons.Rounded.Delete, contentDescription = null) }, + text = { Text(stringResource(R.string.home_menu_delete_group)) }, + onClick = onDeleteGroupClick, + ) + DropdownMenuItem( + leadingIcon = { Icon(Icons.AutoMirrored.Rounded.Sort, contentDescription = null) }, + text = { Text(stringResource(R.string.home_app_bar_sort)) }, + onClick = onSortClick, + ) + DropdownMenuItem( + leadingIcon = { Icon(Icons.Rounded.Settings, contentDescription = null) }, + text = { Text(stringResource(R.string.home_menu_settings)) }, + onClick = onSettingsClick, + ) DropdownMenuItem( leadingIcon = { Icon(Icons.Rounded.Info, contentDescription = null) }, text = { Text(stringResource(R.string.home_menu_about)) }, diff --git a/app/src/main/java/io/github/sds100/keymapper/home/SelectionBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/home/SelectionBottomSheet.kt index c78a40dac2..9add3884a7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/SelectionBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/SelectionBottomSheet.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -67,8 +66,7 @@ fun SelectionBottomSheet( Surface( modifier = modifier .widthIn(max = BottomSheetDefaults.SheetMaxWidth) - .fillMaxWidth() - .navigationBarsPadding(), + .fillMaxWidth(), shadowElevation = 5.dp, shape = BottomSheetDefaults.ExpandedShape, tonalElevation = BottomSheetDefaults.Elevation, 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 10bb5b2747..dc3d19e77c 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 @@ -127,7 +127,7 @@ abstract class SimpleMappingController( } } - private fun performAction( + private suspend fun performAction( action: Action, inputEventType: InputEventType = InputEventType.DOWN_UP, ) { @@ -172,20 +172,22 @@ abstract class SimpleMappingController( } fun reset() { - repeatJobs.values.forEach { jobs -> + for (jobs in repeatJobs.values) { jobs.forEach { it.cancel() } } repeatJobs.clear() - performActionJobs.values.forEach { - it.cancel() + for (job in performActionJobs.values) { + job.cancel() } performActionJobs.clear() - actionsBeingHeldDown.forEach { - performAction(it, InputEventType.UP) + coroutineScope.launch { + for (it in actionsBeingHeldDown) { + performAction(it, InputEventType.UP) + } } actionsBeingHeldDown.clear() diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt index 9c5b0c9390..3814f879f5 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices @@ -58,6 +59,7 @@ import io.github.sds100.keymapper.actions.ActionsScreen import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.constraints.ConstraintsScreen import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerScreen +import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @Composable @@ -141,6 +143,7 @@ private fun ConfigKeyMapScreen( var currentTab: ConfigKeyMapTab? by remember { mutableStateOf(null) } val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current BackHandler(onBack = onBackClick) @@ -167,7 +170,7 @@ private fun ConfigKeyMapScreen( } if (url.isNotEmpty()) { - uriHandler.openUri(url) + uriHandler.openUriSafe(ctx, url) } }, ) @@ -505,6 +508,7 @@ private fun ScreenCard( screen: @Composable () -> Unit, ) { val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current OutlinedCard(modifier = modifier) { Column { @@ -520,7 +524,7 @@ private fun ScreenCard( color = MaterialTheme.colorScheme.primary, ) - IconButton(onClick = { uriHandler.openUri(helpUrl) }) { + IconButton(onClick = { uriHandler.openUriSafe(ctx, helpUrl) }) { Icon( Icons.AutoMirrored.Rounded.HelpOutline, contentDescription = stringResource(R.string.button_help), diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt index f5b0dcdeae..5f69f2bff7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ConfigKeyMapUseCase.kt @@ -52,7 +52,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.util.LinkedList @@ -146,10 +145,6 @@ class ConfigKeyMapUseCaseController( } } - override fun useFloatingButtonTrigger(buttonUid: String) { - floatingButtonToUse.update { buttonUid } - } - override fun addConstraint(constraint: Constraint): Boolean { var containsConstraint = false @@ -309,7 +304,7 @@ class ConfigKeyMapUseCaseController( val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) - val newKeys = trigger.keys.plus(triggerKey) + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } val newMode = when { trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence @@ -340,7 +335,7 @@ class ConfigKeyMapUseCaseController( val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) - val newKeys = trigger.keys.plus(triggerKey) + val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } val newMode = when { trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence @@ -518,7 +513,7 @@ class ConfigKeyMapUseCaseController( // You can't set the trigger to a long press if it contains a key // that isn't detected with key codes. This is because there aren't // separate key events for the up and down press that can be timed. - if (trigger.keys.any { it is AssistantTriggerKey }) { + if (trigger.keys.any { !it.allowedLongPress }) { return@editTrigger trigger } @@ -539,6 +534,10 @@ class ConfigKeyMapUseCaseController( return@editTrigger trigger } + if (trigger.keys.any { !it.allowedDoublePress }) { + return@editTrigger trigger + } + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } val newMode = TriggerMode.Undefined @@ -802,6 +801,10 @@ class ConfigKeyMapUseCaseController( } } + if (data is ActionData.Volume.Down || data is ActionData.Volume.Up || data is ActionData.Volume.Stream) { + repeat = true + } + if (data is ActionData.AnswerCall) { addConstraint(Constraint.PhoneRinging()) } @@ -1025,7 +1028,6 @@ interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { fun getAvailableTriggerKeyDevices(): List - val floatingButtonToUse: StateFlow - fun useFloatingButtonTrigger(buttonUid: String) + val floatingButtonToUse: MutableStateFlow suspend fun getFloatingLayoutCount(): Int } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt index 1a54f071b0..8e87d9d122 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/DisplayKeyMapUseCase.kt @@ -76,10 +76,11 @@ class DisplayKeyMapUseCaseImpl( * This waits for the purchases to be processed with a timeout so the UI doesn't * say there are no purchases while it is loading. */ - private val purchasesFlow: Flow>> = callbackFlow { + private val purchasesFlow: Flow>>> = callbackFlow { try { val value = withTimeout(5000L) { - purchasingManager.purchases.filterIsInstance>>().first() + purchasingManager.purchases.filterIsInstance>>>() + .first() } send(value) @@ -103,7 +104,7 @@ class DisplayKeyMapUseCaseImpl( isKeyMapperImeChosen = keyMapperImeHelper.isCompatibleImeChosen(), isDndAccessGranted = permissionAdapter.isGranted(Permission.ACCESS_NOTIFICATION_POLICY), isRootGranted = permissionAdapter.isGranted(Permission.ROOT), - purchases = purchases.dataOrNull() ?: emptySet(), + purchases = purchases.dataOrNull() ?: Success(emptySet()), showDpadImeSetupError = showDpadImeSetupError, ) } @@ -150,6 +151,8 @@ class DisplayKeyMapUseCaseImpl( ProductId.FLOATING_BUTTONS, ), ) + + TriggerError.PURCHASE_VERIFICATION_FAILED -> purchasingManager.refresh() } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt index 2c46bb79d7..b8f4d46016 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListItemCreator.kt @@ -267,21 +267,25 @@ class KeyMapListItemCreator( } } - if (deviceName != null || key.detectionSource == KeyEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { - append(" (") + val parts = mutableListOf() + if (deviceName != null || key.detectionSource == KeyEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { - append("${getString(R.string.flag_detect_from_input_method)} $midDot ") + parts.add(getString(R.string.flag_detect_from_input_method)) } if (deviceName != null) { - append(deviceName) + parts.add(deviceName) } if (!key.consumeEvent) { - append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") + parts.add(getString(R.string.flag_dont_override_default_action)) } + } + if (parts.isNotEmpty()) { + append(" (") + append(parts.joinToString(separator = " $midDot ")) append(")") } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt index 71d155010d..5cf30faa3c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListScreen.kt @@ -91,7 +91,9 @@ fun KeyMapList( Surface(modifier = modifier) { if (listItems.data.isEmpty()) { EmptyKeyMapList( - modifier = Modifier.fillMaxSize().padding(bottom = bottomListPadding), + modifier = Modifier + .fillMaxSize() + .padding(bottom = bottomListPadding), ) } else { LoadedKeyMapList( @@ -487,6 +489,7 @@ private fun getTriggerErrorMessage(error: TriggerError): String { TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) TriggerError.FLOATING_BUTTON_DELETED -> stringResource(R.string.trigger_error_floating_button_deleted) TriggerError.FLOATING_BUTTONS_NOT_PURCHASED -> stringResource(R.string.trigger_error_floating_buttons_not_purchased) + TriggerError.PURCHASE_VERIFICATION_FAILED -> stringResource(R.string.trigger_error_product_verification_failed) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt index 4352967f2b..5d4d4e4b7c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapListViewModel.kt @@ -26,6 +26,7 @@ import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.sorting.SortViewModel import io.github.sds100.keymapper.system.accessibility.ServiceState +import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result @@ -82,6 +83,7 @@ class KeyMapListViewModel( private val showAlertsUseCase: ShowHomeScreenAlertsUseCase, private val pauseKeyMaps: PauseKeyMapsUseCase, private val backupRestore: BackupRestoreMappingsUseCase, + private val showInputMethodPickerUseCase: ShowInputMethodPickerUseCase, ) : PopupViewModel by PopupViewModelImpl(), ResourceProvider by resourceProvider, @@ -845,6 +847,10 @@ class KeyMapListViewModel( } } + fun showInputMethodPicker() { + showInputMethodPickerUseCase.show(fromForeground = true) + } + private suspend fun onAutomaticBackupResult(result: Result<*>) { when (result) { is Success -> {} diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt index 5f3b5b3f17..03216b2bf0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/KeyMapOptionsScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -48,6 +49,7 @@ import io.github.sds100.keymapper.util.ui.SliderMinimums import io.github.sds100.keymapper.util.ui.SliderStepSizes import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.SliderOptionText +import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @Composable @@ -323,6 +325,7 @@ private fun TriggerFromOtherAppsSection( } val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current val intentGuideUrl = stringResource(R.string.url_trigger_by_intent_guide) FilledTonalButton( @@ -330,7 +333,7 @@ private fun TriggerFromOtherAppsSection( .fillMaxWidth() .padding(horizontal = 16.dp), onClick = { - uriHandler.openUri(intentGuideUrl) + uriHandler.openUriSafe(ctx, intentGuideUrl) }, ) { Text(text = stringResource(R.string.button_open_trigger_keymap_from_intent_guide)) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt index ce2b62b4ad..d8a147549a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/ShortcutRow.kt @@ -38,7 +38,7 @@ fun ShortcutRow( FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( - 16.dp, + 8.dp, alignment = Alignment.CenterHorizontally, ), verticalArrangement = Arrangement.Center, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt index 1ae73e4351..fafed46f55 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/DetectKeyMapsUseCase.kt @@ -32,11 +32,14 @@ import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull import io.github.sds100.keymapper.util.ui.ResourceProvider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber /** @@ -58,6 +61,7 @@ class DetectKeyMapsUseCaseImpl( private val permissionAdapter: PermissionAdapter, private val resourceProvider: ResourceProvider, private val vibrator: VibratorAdapter, + private val coroutineScope: CoroutineScope, ) : DetectKeyMapsUseCase { companion object { @@ -164,6 +168,7 @@ class DetectKeyMapsUseCaseImpl( accessibilityService, shizukuInputEventInjector, permissionAdapter, + coroutineScope, ) override val forceVibrate: Flow = @@ -189,18 +194,20 @@ class DetectKeyMapsUseCaseImpl( inputEventType: InputEventType, scanCode: Int, ) { + val model = InputKeyModel( + keyCode, + inputEventType, + metaState, + deviceId, + scanCode, + ) + if (permissionAdapter.isGranted(Permission.SHIZUKU)) { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with Shizuku, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") - shizukuInputEventInjector.inputKeyEvent( - InputKeyModel( - keyCode, - inputEventType, - metaState, - deviceId, - scanCode, - ), - ) + coroutineScope.launch { + shizukuInputEventInjector.inputKeyEvent(model) + } } else { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") @@ -217,15 +224,9 @@ class DetectKeyMapsUseCaseImpl( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> imeInputEventInjector.inputKeyEvent( - InputKeyModel( - keyCode, - inputEventType, - metaState, - deviceId, - scanCode, - ), - ) + else -> runBlocking { + imeInputEventInjector.inputKeyEvent(model) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt index 8205770d4e..1a8a3ef36d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/ParallelTriggerActionPerformer.kt @@ -57,7 +57,7 @@ class ParallelTriggerActionPerformer( once before repeating (if configured). */ performActionsJob = coroutineScope.launch { - actionList.forEachIndexed { actionIndex, action -> + for ((actionIndex, action) in actionList.withIndex()) { var performUpAction = false if (action.holdDown && action.repeat && action.repeatMode == RepeatMode.TRIGGER_PRESSED_AGAIN) { @@ -94,15 +94,17 @@ class ParallelTriggerActionPerformer( } } - repeatJobs.forEach { it?.cancel() } + for (job in repeatJobs) { + job?.cancel() + } - actionList.forEachIndexed { actionIndex, action -> + for ((actionIndex, action) in actionList.withIndex()) { if (!action.repeat) { - return@forEachIndexed + continue } if (calledOnTriggerRelease && action.repeatMode == RepeatMode.TRIGGER_RELEASED) { - return@forEachIndexed + continue } // don't start repeating if it is already repeating @@ -110,11 +112,11 @@ class ParallelTriggerActionPerformer( repeatJobs[actionIndex]?.cancel() repeatJobs[actionIndex] = null - return@forEachIndexed + continue } if (action.data is ActionData.InputKeyEvent && InputEventUtils.isModifierKey(action.data.keyCode)) { - return@forEachIndexed + continue } repeatJobs[actionIndex] = coroutineScope.launch { @@ -124,9 +126,11 @@ class ParallelTriggerActionPerformer( delay(action.repeatDelay?.toLong() ?: defaultRepeatDelay.value) while (isActive && continueRepeating) { - if (action.holdDown && action.repeat) { + if (action.holdDown) { performAction(action, InputEventType.DOWN, metaState) - delay(action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value) + delay( + action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value, + ) performAction(action, InputEventType.UP, metaState) } else { performAction(action, InputEventType.DOWN_UP, metaState) @@ -152,12 +156,14 @@ class ParallelTriggerActionPerformer( } } - actionList.forEachIndexed { actionIndex, action -> - if (action.holdDown && !action.stopHoldDownWhenTriggerPressedAgain) { - if (actionIsHeldDown[actionIndex]) { - actionIsHeldDown[actionIndex] = false + coroutineScope.launch { + for ((actionIndex, action) in actionList.withIndex()) { + if (action.holdDown && !action.stopHoldDownWhenTriggerPressedAgain) { + if (actionIsHeldDown[actionIndex]) { + actionIsHeldDown[actionIndex] = false - performAction(action, InputEventType.UP, metaState) + performAction(action, InputEventType.UP, metaState) + } } } } @@ -167,9 +173,11 @@ class ParallelTriggerActionPerformer( performActionsJob?.cancel() performActionsJob = null - actionIsHeldDown.forEachIndexed { index, isHeldDown -> - if (isHeldDown) { - performAction(actionList[index], inputEventType = InputEventType.UP, 0) + coroutineScope.launch { + for ((index, isHeldDown) in actionIsHeldDown.withIndex()) { + if (isHeldDown) { + performAction(actionList[index], inputEventType = InputEventType.UP, 0) + } } } @@ -183,7 +191,7 @@ class ParallelTriggerActionPerformer( } } - private fun performAction( + private suspend fun performAction( action: Action, inputEventType: InputEventType, metaState: Int, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt index b776b709b7..a5ef9e2b0d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/detection/SequenceTriggerActionPerformer.kt @@ -25,7 +25,7 @@ class SequenceTriggerActionPerformer( */ job?.cancel() job = coroutineScope.launch { - actionList.forEach { action -> + for (action in actionList) { performAction(action, metaState) delay(action.delayBeforeNextAction?.toLong() ?: 0L) @@ -38,7 +38,7 @@ class SequenceTriggerActionPerformer( job = null } - private fun performAction(action: Action, metaState: Int) { + private suspend fun performAction(action: Action, metaState: Int) { repeat(action.multiplier ?: 1) { useCase.perform(action.data, InputEventType.DOWN_UP, metaState) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt index a4e05c3299..45e37e94e4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/AssistantTriggerKey.kt @@ -23,6 +23,7 @@ data class AssistantTriggerKey( override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = false + override val allowedDoublePress: Boolean = false override fun compareTo(other: TriggerKey) = when (other) { is AssistantTriggerKey -> compareValuesBy( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt index 847da51c4b..34658e5890 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/BaseConfigTriggerViewModel.kt @@ -27,7 +27,9 @@ import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.State import io.github.sds100.keymapper.util.dataOrNull +import io.github.sds100.keymapper.util.ifIsData import io.github.sds100.keymapper.util.mapData +import io.github.sds100.keymapper.util.onSuccess import io.github.sds100.keymapper.util.ui.CheckBoxListItem import io.github.sds100.keymapper.util.ui.DialogResponse import io.github.sds100.keymapper.util.ui.LinkType @@ -93,7 +95,7 @@ abstract class BaseConfigTriggerViewModel( private val triggerKeyShortcuts = combine( fingerprintGesturesSupported.isSupported, purchasingManager.purchases, - ) { isFingerprintGesturesSupported, purchases -> + ) { isFingerprintGesturesSupported, purchasesState -> val newShortcuts = mutableSetOf>() if (isFingerprintGesturesSupported == true) { @@ -106,25 +108,27 @@ abstract class BaseConfigTriggerViewModel( ) } - if (purchases is State.Data) { - if (purchases.data.contains(ProductId.ASSISTANT_TRIGGER)) { - newShortcuts.add( - ShortcutModel( - icon = ComposeIconInfo.Vector(Icons.Rounded.Assistant), - text = getString(R.string.trigger_key_shortcut_add_assistant), - data = TriggerKeyShortcut.ASSISTANT, - ), - ) - } + purchasesState.ifIsData { result -> + result.onSuccess { purchases -> + if (purchases.contains(ProductId.ASSISTANT_TRIGGER)) { + newShortcuts.add( + ShortcutModel( + icon = ComposeIconInfo.Vector(Icons.Rounded.Assistant), + text = getString(R.string.trigger_key_shortcut_add_assistant), + data = TriggerKeyShortcut.ASSISTANT, + ), + ) + } - if (purchases.data.contains(ProductId.FLOATING_BUTTONS)) { - newShortcuts.add( - ShortcutModel( - icon = ComposeIconInfo.Vector(Icons.Rounded.BubbleChart), - text = getString(R.string.trigger_key_shortcut_add_floating_button), - data = TriggerKeyShortcut.FLOATING_BUTTON, - ), - ) + if (purchases.contains(ProductId.FLOATING_BUTTONS)) { + newShortcuts.add( + ShortcutModel( + icon = ComposeIconInfo.Vector(Icons.Rounded.BubbleChart), + text = getString(R.string.trigger_key_shortcut_add_floating_button), + data = TriggerKeyShortcut.FLOATING_BUTTON, + ), + ) + } } } @@ -287,7 +291,7 @@ abstract class BaseConfigTriggerViewModel( * or there are only key code keys in the trigger. It is not possible to do a long press of * non-key code keys in a parallel trigger. */ - if (trigger.keys.size == 1) { + if (trigger.keys.size == 1 && trigger.keys.all { it.allowedDoublePress }) { clickTypeButtons.add(ClickType.SHORT_PRESS) clickTypeButtons.add(ClickType.DOUBLE_PRESS) } @@ -359,7 +363,6 @@ abstract class BaseConfigTriggerViewModel( return TriggerKeyOptionsState.Assistant( assistantType = key.type, clickType = key.clickType, - showClickTypes = showClickTypes, ) } @@ -375,7 +378,6 @@ abstract class BaseConfigTriggerViewModel( return TriggerKeyOptionsState.FingerprintGesture( gestureType = key.type, clickType = key.clickType, - showClickTypes = showClickTypes, ) } } @@ -860,16 +862,16 @@ sealed class TriggerKeyOptionsState { data class Assistant( val assistantType: AssistantTriggerType, override val clickType: ClickType, - override val showClickTypes: Boolean, ) : TriggerKeyOptionsState() { + override val showClickTypes: Boolean = false override val showLongPressClickType: Boolean = false } data class FingerprintGesture( val gestureType: FingerprintGestureType, override val clickType: ClickType, - override val showClickTypes: Boolean, ) : TriggerKeyOptionsState() { + override val showClickTypes: Boolean = false override val showLongPressClickType: Boolean = false } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt index 88204c82e0..1ec7fae6f4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FingerprintTriggerKey.kt @@ -19,6 +19,7 @@ data class FingerprintTriggerKey( ) : TriggerKey() { override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = false + override val allowedDoublePress: Boolean = false override fun compareTo(other: TriggerKey) = when (other) { is FingerprintTriggerKey -> compareValuesBy( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt index 2999fc2cb6..ca23629e92 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/FloatingButtonKey.kt @@ -19,6 +19,7 @@ data class FloatingButtonKey( override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = true + override val allowedDoublePress: Boolean = true override fun compareTo(other: TriggerKey) = when (other) { is FloatingButtonKey -> compareValuesBy( diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt index b8f28e9a53..94b8c0f579 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/KeyCodeTriggerKey.kt @@ -19,6 +19,7 @@ data class KeyCodeTriggerKey( ) : TriggerKey() { override val allowedLongPress: Boolean = true + override val allowedDoublePress: Boolean = true override fun toString(): String { val deviceString = when (device) { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt index 767c250ffd..51c21bd786 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardBottomSheet.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -34,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -101,6 +103,7 @@ private fun SetupGuiKeyboardBottomSheet( ) { val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current val scrollState = rememberScrollState() ModalBottomSheet( @@ -145,7 +148,7 @@ private fun SetupGuiKeyboardBottomSheet( buttonTextEnabled = stringResource(R.string.setup_gui_keyboard_install_keyboard_button), buttonTextDisabled = stringResource(R.string.setup_gui_keyboard_install_keyboard_button_disabled), onButtonClick = { - uriHandler.openUri(guiKeyboardUrl) + uriHandler.openUriSafe(ctx, guiKeyboardUrl) }, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt index c622594308..a3fa5cc5e1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerError.kt @@ -20,4 +20,6 @@ enum class TriggerError(val isFixable: Boolean) { FLOATING_BUTTON_DELETED(isFixable = false), FLOATING_BUTTONS_NOT_PURCHASED(isFixable = true), + + PURCHASE_VERIFICATION_FAILED(isFixable = true), } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt index 9b4f53365a..7b4fe3910d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerErrorSnapshot.kt @@ -6,6 +6,10 @@ import io.github.sds100.keymapper.mappings.keymaps.KeyMap import io.github.sds100.keymapper.mappings.keymaps.requiresImeKeyEventForwardingInPhoneCall import io.github.sds100.keymapper.purchasing.ProductId import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.util.Error +import io.github.sds100.keymapper.util.Result +import io.github.sds100.keymapper.util.onFailure +import io.github.sds100.keymapper.util.onSuccess /** * Store the data required for determining trigger errors to reduce the number of calls with @@ -15,7 +19,7 @@ data class TriggerErrorSnapshot( val isKeyMapperImeChosen: Boolean, val isDndAccessGranted: Boolean, val isRootGranted: Boolean, - val purchases: Set, + val purchases: Result>, val showDpadImeSetupError: Boolean, ) { companion object { @@ -26,13 +30,21 @@ data class TriggerErrorSnapshot( } fun getTriggerError(keyMap: KeyMap, key: TriggerKey): TriggerError? { - if (key is AssistantTriggerKey && !purchases.contains(ProductId.ASSISTANT_TRIGGER)) { - return TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED + purchases.onSuccess { purchases -> + if (key is AssistantTriggerKey && !purchases.contains(ProductId.ASSISTANT_TRIGGER)) { + return TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED + } + + if (key is FloatingButtonKey && !purchases.contains(ProductId.FLOATING_BUTTONS)) { + return TriggerError.FLOATING_BUTTONS_NOT_PURCHASED + } + }.onFailure { error -> + if ((key is AssistantTriggerKey || key is FloatingButtonKey) && error == Error.PurchasingError.NetworkError) { + return TriggerError.PURCHASE_VERIFICATION_FAILED + } } - if (key is FloatingButtonKey && !purchases.contains(ProductId.FLOATING_BUTTONS)) { - return TriggerError.FLOATING_BUTTONS_NOT_PURCHASED - } else if (key is FloatingButtonKey && key.button == null) { + if (key is FloatingButtonKey && key.button == null) { return TriggerError.FLOATING_BUTTON_DELETED } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt index cc29746511..a94d0db410 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKey.kt @@ -15,6 +15,7 @@ sealed class TriggerKey : Comparable { abstract val consumeEvent: Boolean abstract val uid: String abstract val allowedLongPress: Boolean + abstract val allowedDoublePress: Boolean fun setClickType(clickType: ClickType): TriggerKey = when (this) { is AssistantTriggerKey -> copy(clickType = clickType) diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt index 6c95e170a1..46f27f7661 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyListItem.kt @@ -259,6 +259,7 @@ private fun getErrorMessage(error: TriggerError): String { TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) TriggerError.FLOATING_BUTTON_DELETED -> stringResource(R.string.trigger_error_floating_button_deleted) TriggerError.FLOATING_BUTTONS_NOT_PURCHASED -> stringResource(R.string.trigger_error_floating_buttons_not_purchased) + TriggerError.PURCHASE_VERIFICATION_FAILED -> stringResource(R.string.trigger_error_product_verification_failed) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt index 81197a6383..f1fb647ca7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/TriggerKeyOptionsBottomSheet.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -40,6 +41,7 @@ import io.github.sds100.keymapper.mappings.FingerprintGestureType import io.github.sds100.keymapper.util.ui.CheckBoxListItem import io.github.sds100.keymapper.util.ui.compose.CheckBoxText import io.github.sds100.keymapper.util.ui.compose.RadioButtonText +import io.github.sds100.keymapper.util.ui.compose.openUriSafe import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -65,6 +67,7 @@ fun TriggerKeyOptionsBottomSheet( dragHandle = {}, ) { val uriHandler = LocalUriHandler.current + val ctx = LocalContext.current val helpUrl = stringResource(R.string.url_trigger_key_options_guide) val scope = rememberCoroutineScope() @@ -82,7 +85,7 @@ fun TriggerKeyOptionsBottomSheet( modifier = Modifier .align(Alignment.TopEnd) .padding(horizontal = 8.dp), - onClick = { uriHandler.openUri(helpUrl) }, + onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, ) { Icon( imageVector = Icons.AutoMirrored.Rounded.HelpOutline, @@ -330,7 +333,6 @@ private fun AssistantPreview() { state = TriggerKeyOptionsState.Assistant( assistantType = AssistantTriggerType.VOICE, clickType = ClickType.DOUBLE_PRESS, - showClickTypes = true, ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt b/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt index 22ba146008..7ad6b4c003 100644 --- a/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt +++ b/app/src/main/java/io/github/sds100/keymapper/purchasing/PurchasingManager.kt @@ -7,8 +7,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow interface PurchasingManager { val onCompleteProductPurchase: MutableSharedFlow - val purchases: Flow>> + val purchases: Flow>>> suspend fun launchPurchasingFlow(product: ProductId): Result suspend fun getProductPrice(product: ProductId): Result suspend fun isPurchased(product: ProductId): Result + fun refresh() } diff --git a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt index 39f8257540..2d50fd4b7c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/reroutekeyevents/RerouteKeyEventsUseCase.kt @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.util.firstBlocking import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking /** * Created by sds100 on 27/04/2021. @@ -48,7 +49,10 @@ class RerouteKeyEventsUseCaseImpl( } override fun inputKeyEvent(keyModel: InputKeyModel) { - imeInputEventInjector.inputKeyEvent(keyModel) + // It is safe to run the ime injector on the main thread. + runBlocking { + imeInputEventInjector.inputKeyEvent(keyModel) + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt index 6fabff179c..9608b58f1c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/shizuku/ShizukuInputEventInjector.kt @@ -8,15 +8,20 @@ import android.view.KeyEvent import io.github.sds100.keymapper.system.inputevents.InputEventInjector import io.github.sds100.keymapper.system.inputmethod.InputKeyModel import io.github.sds100.keymapper.util.InputEventType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper import timber.log.Timber @SuppressLint("PrivateApi") -class ShizukuInputEventInjector : InputEventInjector { +class ShizukuInputEventInjector(private val coroutineScope: CoroutineScope) : InputEventInjector { companion object { - private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 + // private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 + + private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 } private val iInputManager: IInputManager by lazy { @@ -25,7 +30,7 @@ class ShizukuInputEventInjector : InputEventInjector { IInputManager.Stub.asInterface(binder) } - override fun inputKeyEvent(model: InputKeyModel) { + override suspend fun inputKeyEvent(model: InputKeyModel) { Timber.d("Inject input event with Shizuku ${KeyEvent.keyCodeToString(model.keyCode)}, $model") val action = when (model.inputType) { @@ -46,12 +51,17 @@ class ShizukuInputEventInjector : InputEventInjector { model.scanCode, ) - iInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_ASYNC) + withContext(Dispatchers.IO) { + // MUST wait for the application to finish processing the event before sending the next one. + // Otherwise, rapidly repeating input events will go in a big queue and all inputs + // into the application will be delayed or overloaded. + iInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) - if (model.inputType == InputEventType.DOWN_UP) { - val upEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP) + if (model.inputType == InputEventType.DOWN_UP) { + val upEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP) - iInputManager.injectInputEvent(upEvent, INJECT_INPUT_EVENT_MODE_ASYNC) + iInputManager.injectInputEvent(upEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index 51252b41d7..f29c038ee8 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt @@ -150,10 +150,6 @@ abstract class BaseAccessibilityServiceController( flags = flags.withFlag(AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - flags = flags.withFlag(AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR) - } - return@lazy flags } @@ -495,7 +491,11 @@ abstract class BaseAccessibilityServiceController( } } - is ServiceEvent.TestAction -> performActionsUseCase.perform(event.action) + is ServiceEvent.TestAction -> coroutineScope.launch { + performActionsUseCase.perform( + event.action, + ) + } is ServiceEvent.Ping -> coroutineScope.launch { outputEvents.emit(ServiceEvent.Pong(event.key)) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt index 449f83c0fa..59d9ac5e42 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/IAccessibilityService.kt @@ -61,7 +61,4 @@ interface IAccessibilityService { fun disableSelf() fun findFocussedNode(focus: Int): AccessibilityNodeModel? - - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - fun inputText(text: String) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt index 54e3538019..2fdae08f4f 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/MyAccessibilityService.kt @@ -562,12 +562,6 @@ class MyAccessibilityService : override fun findFocussedNode(focus: Int): AccessibilityNodeModel? = findFocus(focus)?.toModel() - override fun inputText(text: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - inputMethod?.currentInputConnection?.commitText(text, 1, null) - } - } - override fun setInputMethodEnabled(imeId: String, enabled: Boolean) { @SuppressLint("CheckResult") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index ebed7f99f2..daa1799a9a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import splitties.mainthread.mainLooper @@ -109,6 +110,12 @@ class AndroidDevicesAdapter( connectedBluetoothDevices.value = currentValue.minus(device) }.launchIn(coroutineScope) + + bluetoothAdapter.isBluetoothEnabled.onEach { isEnabled -> + if (!isEnabled) { + connectedBluetoothDevices.update { emptySet() } + } + }.launchIn(coroutineScope) } override fun deviceHasKey(id: Int, keyCode: Int): Boolean { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt index 97bbe5764a..a126bac316 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt @@ -2,10 +2,6 @@ package io.github.sds100.keymapper.system.inputevents import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -/** - * Created by sds100 on 21/04/2021. - */ - interface InputEventInjector { - fun inputKeyEvent(model: InputKeyModel) + suspend fun inputKeyEvent(model: InputKeyModel) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt index 298dfc076d..756acde8df 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/ImeInputEventInjector.kt @@ -45,7 +45,7 @@ class ImeInputEventInjectorImpl( private val ctx = context.applicationContext - override fun inputKeyEvent(model: InputKeyModel) { + override suspend fun inputKeyEvent(model: InputKeyModel) { Timber.d("Inject key event with input method ${KeyEvent.keyCodeToString(model.keyCode)}, $model") val imePackageName = inputMethodAdapter.chosenIme.value?.packageName diff --git a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt index aa24829d97..0864806c2d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/navigation/OpenMenuHelper.kt @@ -13,6 +13,8 @@ import io.github.sds100.keymapper.util.InputEventType import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.firstBlocking import io.github.sds100.keymapper.util.success +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Created by sds100 on 21/04/2021. @@ -22,6 +24,7 @@ class OpenMenuHelper( private val accessibilityService: IAccessibilityService, private val shizukuInputEventInjector: InputEventInjector, private val permissionAdapter: PermissionAdapter, + private val coroutineScope: CoroutineScope, ) { companion object { @@ -36,7 +39,9 @@ class OpenMenuHelper( inputType = InputEventType.DOWN_UP, ) - shizukuInputEventInjector.inputKeyEvent(inputKeyModel) + coroutineScope.launch { + shizukuInputEventInjector.inputKeyEvent(inputKeyModel) + } return success() } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/tiles/ToggleMappingsTile.kt b/app/src/main/java/io/github/sds100/keymapper/system/tiles/ToggleMappingsTile.kt index e838c840e5..7763983b09 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/tiles/ToggleMappingsTile.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/tiles/ToggleMappingsTile.kt @@ -45,35 +45,83 @@ class ToggleMappingsTile : val ctx = this@ToggleMappingsTile - when { - serviceState == ServiceState.DISABLED -> { - qsTile.label = str(R.string.tile_service_disabled) - qsTile.contentDescription = - str(R.string.tile_accessibility_service_disabled_content_description) - qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_error) - qsTile.state = Tile.STATE_UNAVAILABLE - } - - isPaused -> { - qsTile.label = str(R.string.tile_resume) - qsTile.contentDescription = str(R.string.tile_resume) - qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_resume) - qsTile.state = Tile.STATE_INACTIVE - } - - !isPaused -> { - qsTile.label = str(R.string.tile_pause) - qsTile.contentDescription = str(R.string.tile_pause) - qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_pause) - qsTile.state = Tile.STATE_ACTIVE - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + updateQsTile(serviceState, ctx, isPaused) + } else { + updateQsTilePreSdk29(serviceState, ctx, isPaused) } - - qsTile.updateTile() }.collect() } } + private fun updateQsTilePreSdk29( + serviceState: ServiceState, + ctx: ToggleMappingsTile, + isPaused: Boolean, + ) { + when { + serviceState == ServiceState.DISABLED -> { + qsTile.label = str(R.string.tile_service_disabled) + qsTile.contentDescription = + str(R.string.tile_accessibility_service_disabled_content_description) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_error) + qsTile.state = Tile.STATE_UNAVAILABLE + } + + isPaused -> { + qsTile.label = str(R.string.tile_resume_title) + qsTile.contentDescription = str(R.string.tile_resume_title) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_pause) + qsTile.state = Tile.STATE_INACTIVE + } + + !isPaused -> { + qsTile.label = str(R.string.tile_pause_title) + qsTile.contentDescription = str(R.string.tile_pause_title) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_resume) + qsTile.state = Tile.STATE_ACTIVE + } + } + + qsTile.updateTile() + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun updateQsTile( + serviceState: ServiceState, + ctx: ToggleMappingsTile, + isPaused: Boolean, + ) { + when { + serviceState == ServiceState.DISABLED -> { + qsTile.label = str(R.string.app_name) + qsTile.subtitle = str(R.string.tile_service_disabled) + qsTile.contentDescription = + str(R.string.tile_accessibility_service_disabled_content_description) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_error) + qsTile.state = Tile.STATE_UNAVAILABLE + } + + isPaused -> { + qsTile.label = str(R.string.app_name) + qsTile.subtitle = str(R.string.tile_paused_subtitle) + qsTile.contentDescription = str(R.string.tile_resume_title) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_pause) + qsTile.state = Tile.STATE_INACTIVE + } + + !isPaused -> { + qsTile.label = str(R.string.app_name) + qsTile.subtitle = str(R.string.tile_running_subtitle) + qsTile.contentDescription = str(R.string.tile_pause_title) + qsTile.icon = Icon.createWithResource(ctx, R.drawable.ic_tile_resume) + qsTile.state = Tile.STATE_ACTIVE + } + } + + qsTile.updateTile() + } + override fun onStartListening() { lifecycleRegistry.currentState = Lifecycle.State.STARTED diff --git a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt index f24c2b27b3..41a4f69d47 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/Inject.kt @@ -39,6 +39,7 @@ import io.github.sds100.keymapper.system.apps.ChooseAppViewModel import io.github.sds100.keymapper.system.apps.DisplayAppShortcutsUseCaseImpl import io.github.sds100.keymapper.system.bluetooth.ChooseBluetoothDeviceUseCaseImpl import io.github.sds100.keymapper.system.bluetooth.ChooseBluetoothDeviceViewModel +import io.github.sds100.keymapper.system.inputmethod.ShowInputMethodPickerUseCaseImpl import io.github.sds100.keymapper.system.intents.ConfigIntentViewModel /** @@ -182,6 +183,7 @@ object Inject { UseCases.displayKeyMap(ctx), ), UseCases.listFloatingLayouts(ctx), + ShowInputMethodPickerUseCaseImpl(ServiceLocator.inputMethodAdapter(ctx)), ) fun settingsViewModel(context: Context): SettingsViewModel.Factory = SettingsViewModel.Factory( diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ListUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ListUtils.kt index 4eb60c35ee..429f0b7a48 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ListUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ListUtils.kt @@ -7,6 +7,10 @@ import java.util.Collections */ fun MutableList<*>.moveElement(fromIndex: Int, toIndex: Int) { + if (toIndex >= size || fromIndex >= size) { + return + } + if (fromIndex < toIndex) { for (i in fromIndex until toIndex) { Collections.swap(this, i, i + 1) diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/UriHandlerUtils.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/UriHandlerUtils.kt new file mode 100644 index 0000000000..59b70ff257 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/UriHandlerUtils.kt @@ -0,0 +1,15 @@ +package io.github.sds100.keymapper.util.ui.compose + +import android.content.Context +import android.widget.Toast +import androidx.compose.ui.platform.UriHandler +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.util.str + +fun UriHandler.openUriSafe(ctx: Context, uri: String) { + try { + openUri(uri) + } catch (e: IllegalArgumentException) { + Toast.makeText(ctx, ctx.str(R.string.error_no_app_to_open_url), Toast.LENGTH_SHORT).show() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dfd8e9db02..a95ad25d6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -761,8 +761,10 @@ - Pause key maps - Resume key maps + Pause key maps + Resume key maps + Paused + Running Service Disabled Key Mapper accessibility service is disabled Toggle Key Mapper keyboard @@ -1212,6 +1214,7 @@ You must read the instructions on our website that describe how to set up this trigger. Key Mapper will not guide you. Read instructions Select trigger type + Purchase can not be verified. Do you have an internet connection? Unlock (%s) @@ -1355,6 +1358,7 @@ About Export all Import + Choose keyboard Importing… Importing successful! Exporting… 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 2bfa190c60..a76df90b1a 100644 --- a/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/BackupManagerTest.kt @@ -5,6 +5,7 @@ import com.google.gson.Gson import com.google.gson.JsonParser import io.github.sds100.keymapper.actions.sound.SoundFileInfo import io.github.sds100.keymapper.actions.sound.SoundsManager +import io.github.sds100.keymapper.backup.BackupContent import io.github.sds100.keymapper.backup.BackupManagerImpl import io.github.sds100.keymapper.backup.RestoreType import io.github.sds100.keymapper.data.db.AppDatabase @@ -46,10 +47,12 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any import org.mockito.kotlin.anyVararg import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -79,6 +82,7 @@ class BackupManagerTest { private lateinit var fakeFileAdapter: FakeFileAdapter private lateinit var fakePreferenceRepository: PreferenceRepository private lateinit var mockKeyMapRepository: KeyMapRepository + private lateinit var mockGroupRepository: GroupRepository private lateinit var mockSoundsManager: SoundsManager private lateinit var mockUuidGenerator: UuidGenerator @@ -94,6 +98,10 @@ class BackupManagerTest { fakePreferenceRepository = FakePreferenceRepository() mockKeyMapRepository = mock() + mockGroupRepository = mock { + on { getAllGroups() } doReturn MutableStateFlow(emptyList()) + on { getGroupsByParent(ArgumentMatchers.any()) }.thenReturn(MutableStateFlow(emptyList())) + } fakeFileAdapter = FakeFileAdapter(temporaryFolder) @@ -116,9 +124,7 @@ class BackupManagerTest { floatingLayoutRepository = mock { on { layouts } doReturn MutableStateFlow(State.Data(emptyList())) }, - groupRepository = mock { - on { getAllGroups() } doReturn MutableStateFlow(emptyList()) - }, + groupRepository = mockGroupRepository, ) parser = JsonParser() @@ -130,6 +136,62 @@ class BackupManagerTest { Dispatchers.resetMain() } + /** + * Issue #1655. If the list of groups in the backup has a child before the parent then the + * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. + */ + @Test + fun `restore groups breadth first so parents exist before children are restored`() = runTest(testDispatcher) { + val parentGroup1 = GroupEntity( + uid = "parent_group_1_uid", + name = "parent_group_1_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val parentGroup2 = GroupEntity( + uid = "parent_group_2_uid", + name = "parent_group_2_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val childGroup = GroupEntity( + uid = "child_group_uid", + name = "child_group_name", + parentUid = parentGroup1.uid, + lastOpenedDate = 0L, + ) + + val grandChildGroup = GroupEntity( + uid = "grand_child_group_uid", + name = "grand_child_group_name", + parentUid = childGroup.uid, + lastOpenedDate = 0L, + ) + + val backupContent = BackupContent( + appVersion = Constants.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(parentGroup2, grandChildGroup, childGroup, parentGroup1), + ) + + inOrder(mockGroupRepository) { + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, + ) + + verify(mockGroupRepository).insert(parentGroup1) + verify(mockGroupRepository).insert(childGroup) + verify(mockGroupRepository).insert(grandChildGroup) + verify(mockGroupRepository).insert(parentGroup2) + verify(mockGroupRepository, never()).update(any()) + } + } + @Test fun `when backing up everything include layouts that are not in the list of key maps`() = runTest(testDispatcher) { val layoutWithButtons = FloatingLayoutEntityWithButtons( 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 6d2300f589..fa87b9068a 100644 --- a/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt +++ b/app/src/test/java/io/github/sds100/keymapper/ConfigKeyMapUseCaseTest.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.actions.Action import io.github.sds100.keymapper.actions.ActionData import io.github.sds100.keymapper.constraints.Constraint import io.github.sds100.keymapper.mappings.ClickType +import io.github.sds100.keymapper.mappings.FingerprintGestureType import io.github.sds100.keymapper.mappings.keymaps.ConfigKeyMapUseCaseController import io.github.sds100.keymapper.mappings.keymaps.KeyMap import io.github.sds100.keymapper.mappings.keymaps.trigger.AssistantTriggerKey @@ -57,6 +58,150 @@ class ConfigKeyMapUseCaseTest { ) } + @Test + fun `Do not allow setting double press for parallel trigger with side key`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerDoublePress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for parallel trigger with side key`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerLongPress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting double press for side key`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerDoublePress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Undefined)) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for side key`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + useCase.setTriggerLongPress() + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Undefined)) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to double press volume button`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + + useCase.setTriggerDoublePress() + + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to double press volume button`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + + useCase.setTriggerDoublePress() + + useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to long press volume button`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + + useCase.setTriggerLongPress() + + useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to long press volume button`() = runTest(testDispatcher) { + useCase.keyMap.value = State.Data(KeyMap()) + + useCase.addKeyCodeTriggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + TriggerKeyDevice.Any, + detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + + useCase.setTriggerLongPress() + + useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) + + val trigger = useCase.keyMap.value.dataOrNull()!!.trigger + assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + @Test fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = runTest(testDispatcher) { useCase.keyMap.value = State.Data(KeyMap()) diff --git a/app/version.properties b/app/version.properties index b2195383b2..aa81264386 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.0.0 -VERSION_CODE=95 +VERSION_NAME=3.0.1 +VERSION_CODE=103 VERSION_NUM=0 \ No newline at end of file diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/ar/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ar/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/ar/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/full_description.txt b/fastlane/metadata/android/cs_CZ/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/cs_CZ/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/short_description.txt b/fastlane/metadata/android/cs_CZ/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/cs_CZ/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/cs_CZ/title.txt b/fastlane/metadata/android/cs_CZ/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/cs_CZ/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/full_description.txt b/fastlane/metadata/android/de_DE/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/de_DE/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/short_description.txt b/fastlane/metadata/android/de_DE/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/de_DE/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/de_DE/title.txt b/fastlane/metadata/android/de_DE/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/de_DE/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/95.txt b/fastlane/metadata/android/en-US/changelogs/95.txt index e7303018d8..7c29bd65d8 100644 --- a/fastlane/metadata/android/en-US/changelogs/95.txt +++ b/fastlane/metadata/android/en-US/changelogs/95.txt @@ -1,13 +1,13 @@ Key Mapper 3.0 is here! 🎉 -🫧 This release introduces Floating Buttons: you can create custom on-screen buttons to trigger key maps. +🫧 Floating Buttons: you can create custom on-screen buttons to trigger key maps. 🗂️ Grouping key maps into folders with shared constraints. -🔦 You can now change the flashlight brightness. Tip: use the constraint for when the flashlight is showing to remap your volume buttons to change the brightness. +🔦 Change the flashlight brightness. Tip: use the constraint for when the flashlight is showing to remap your volume buttons to change the brightness. 🛜 Send HTTP requests with a new action. -❤️ There are also tonnes of improvements to make your key mapping experience more enjoyable. +❤️ Many improvements to make your key mapping experience more enjoyable. See all the changes at http://changelog.keymapper.club. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index c15c478bd3..6863b48341 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -33,6 +33,18 @@ Not currently supported: - Mouse buttons - Joysticks and triggers (LT,RT) on gamepads + +Security and accessibility services +--------------------------- + +This app includes our Key Mapper Accessibility service that uses the Android Accessibility API to detect the app in focus and adapt key presses to user-defined key maps. It is also used to draw assistive Floating Button overlays on top of other apps. + +By accepting to run the accessibility service, the app will monitor key strokes while you're using your device. It will also emulate swipes and pinches if you are using those actions in the app. + +It will NOT collect any user data or connect to the internet to send any data anywhere. + +Our accessibility service is only triggered by the user when pressing a physical key on their device. It can be turned off any time by the user in the system accessibility settings. + Come say hi in our Discord community! www.keymapper.club diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt index 6eb7f19549..4efb85f341 100644 --- a/fastlane/metadata/android/en-US/title.txt +++ b/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -Key Mapper: Unleash your keys! \ No newline at end of file +Key Mapper & Floating buttons \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/full_description.txt b/fastlane/metadata/android/es_ES/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/es_ES/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/short_description.txt b/fastlane/metadata/android/es_ES/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/es_ES/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/es_ES/title.txt b/fastlane/metadata/android/es_ES/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/es_ES/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/full_description.txt b/fastlane/metadata/android/fr_FR/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/fr_FR/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/short_description.txt b/fastlane/metadata/android/fr_FR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/fr_FR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/fr_FR/title.txt b/fastlane/metadata/android/fr_FR/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/fr_FR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/full_description.txt b/fastlane/metadata/android/hu_HU/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/hu_HU/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/short_description.txt b/fastlane/metadata/android/hu_HU/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/hu_HU/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/hu_HU/title.txt b/fastlane/metadata/android/hu_HU/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/hu_HU/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/full_description.txt b/fastlane/metadata/android/ka_GE/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/ka_GE/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/short_description.txt b/fastlane/metadata/android/ka_GE/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ka_GE/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ka_GE/title.txt b/fastlane/metadata/android/ka_GE/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/ka_GE/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/full_description.txt b/fastlane/metadata/android/ko_KR/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/ko_KR/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/short_description.txt b/fastlane/metadata/android/ko_KR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ko_KR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ko_KR/title.txt b/fastlane/metadata/android/ko_KR/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/ko_KR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/full_description.txt b/fastlane/metadata/android/pl_PL/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/pl_PL/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/short_description.txt b/fastlane/metadata/android/pl_PL/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/pl_PL/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pl_PL/title.txt b/fastlane/metadata/android/pl_PL/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/pl_PL/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/full_description.txt b/fastlane/metadata/android/pt_BR/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/pt_BR/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/short_description.txt b/fastlane/metadata/android/pt_BR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/pt_BR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/pt_BR/title.txt b/fastlane/metadata/android/pt_BR/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/pt_BR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/full_description.txt b/fastlane/metadata/android/ru_RU/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/ru_RU/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/short_description.txt b/fastlane/metadata/android/ru_RU/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/ru_RU/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/ru_RU/title.txt b/fastlane/metadata/android/ru_RU/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/ru_RU/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/sk/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/sk/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/sk/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/full_description.txt b/fastlane/metadata/android/tr_TR/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/tr_TR/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/short_description.txt b/fastlane/metadata/android/tr_TR/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/tr_TR/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/tr_TR/title.txt b/fastlane/metadata/android/tr_TR/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/tr_TR/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/uk/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/uk/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/uk/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/vi/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/vi/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/vi/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/full_description.txt b/fastlane/metadata/android/zh_CN/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/zh_CN/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/short_description.txt b/fastlane/metadata/android/zh_CN/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/zh_CN/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_CN/title.txt b/fastlane/metadata/android/zh_CN/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/zh_CN/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/full_description.txt b/fastlane/metadata/android/zh_TW/full_description.txt deleted file mode 100644 index c15c478bd3..0000000000 --- a/fastlane/metadata/android/zh_TW/full_description.txt +++ /dev/null @@ -1,43 +0,0 @@ -Make custom macros on your keyboard or gamepad, make on-screen buttons in any app, and unlock new functionality from your volume buttons! - -Key Mapper supports a huge variety of buttons and keys*: - -- ALL your phone buttons (volume AND side key) -- Game controllers (D-pad, ABXY, and most others) -- Keyboards -- Headsets and headphones -- Fingerprint sensor - -Not enough keys? Design your own on-screen button layouts and remap those just like real keys! - - -What shortcuts can I make? --------------------------- - -With over 100 individual actions, the sky is the limit. -Build complex macros with screen taps and gestures, keyboard inputs, open apps, control media, and even send intents directly to other apps. - - -How much control do I have? ---------------------------- - -TRIGGERS: You decide how to trigger a key map. Long press, double press, press as many times as you like! Combine keys on different devices, and even include your on-screen buttons. - -ACTIONS: Design specific macros for what you want to do. Combine over 100 actions, and choose the delay between each one. Set repeating actions to automate and speed up slow tasks. - -CONSTRAINTS: You choose when key maps should run and when they shouldn't. Only need it in one specific app? Or when media is playing? On your lockscreen? Constrain your key maps for maximum control. - -* Most devices are already supported, with new devices being added over time. Let us know if it's not working for you and we can prioritize your device. - -Not currently supported: - - Mouse buttons - - Joysticks and triggers (LT,RT) on gamepads - -Come say hi in our Discord community! -www.keymapper.club - -See the code for yourself! (Open source) -code.keymapper.club - -Read the documentation: -docs.keymapper.club \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/short_description.txt b/fastlane/metadata/android/zh_TW/short_description.txt deleted file mode 100644 index ecaa2a662d..0000000000 --- a/fastlane/metadata/android/zh_TW/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Make shortcuts for ANYTHING! Remap volume, power, keyboard, or floating buttons! \ No newline at end of file diff --git a/fastlane/metadata/android/zh_TW/title.txt b/fastlane/metadata/android/zh_TW/title.txt deleted file mode 100644 index 6eb7f19549..0000000000 --- a/fastlane/metadata/android/zh_TW/title.txt +++ /dev/null @@ -1 +0,0 @@ -Key Mapper: Unleash your keys! \ No newline at end of file