diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed837fe12..6adf831157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [3.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.4) + +#### 2 April 2025 + +_See the changes from previous 3.0 Beta releases as well._ + +## Added + +- #1620 enable Key Mapper Basic Input Method without user interaction on Android 13+. +- #1619 Automatically select the non key mapper keyboard when the device is locked and wanting to type. + +## Changed + +- *Finally* renamed the theme settings after many years. @jambl3r. + +## Bug fixes + +- #1618, #1532, #1590 The Key Mapper keyboard is no longer required for Text actions. +- Flashlight action works again on devices that do not support variable brightness + ## [3.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.3) _See the changes from previous 3.0 Beta releases as well._ diff --git a/app/build.gradle b/app/build.gradle index 117a9d541a..f148f0d864 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,7 +165,6 @@ dependencies { def room_version = "2.6.1" def coroutinesVersion = "1.9.0" def nav_version = '2.8.9' - def work_version = "2.10.0" def epoxy_version = "4.6.2" def splitties_version = "3.0.0" def multidex_version = "2.0.1" @@ -211,7 +210,6 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7" implementation "androidx.room:room-ktx:$room_version" - implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.multidex:multidex:$multidex_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8181ee9028..c581f61ca5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:directBootAware="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme.NoActionBar" @@ -174,6 +175,7 @@ android:exported="true"> + @@ -231,8 +233,10 @@ android:resource="@xml/config_accessibility_service" /> + diff --git a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt index 92ddd731f7..b5b3275dc6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt +++ b/app/src/main/java/io/github/sds100/keymapper/KeyMapperApp.kt @@ -1,8 +1,12 @@ package io.github.sds100.keymapper +import android.annotation.SuppressLint import android.content.Intent import android.os.Build +import android.os.UserManager +import android.util.Log import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent @@ -64,7 +68,10 @@ import java.util.Calendar /** * Created by sds100 on 19/05/2020. */ +@SuppressLint("LogNotTimber") class KeyMapperApp : MultiDexApplication() { + private val tag = KeyMapperApp::class.simpleName + val appCoroutineScope = MainScope() val notificationAdapter by lazy { AndroidNotificationAdapter(this, appCoroutineScope) } @@ -167,9 +174,16 @@ class KeyMapperApp : MultiDexApplication() { private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } + private val userManager: UserManager? by lazy { getSystemService() } + + private val initLock: Any = Any() + private var initialized = false + override fun onCreate() { val priorExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() + Log.i(tag, "KeyMapperApp: OnCreate") + Thread.setDefaultUncaughtExceptionHandler { thread, exception -> // log in a blocking manner and always log regardless of whether the setting is turned on val entry = LogEntryEntity( @@ -188,9 +202,30 @@ class KeyMapperApp : MultiDexApplication() { super.onCreate() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { -// DynamicColors.applyToActivitiesIfAvailable(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && userManager?.isUserUnlocked == false) { + Log.i(tag, "KeyMapperApp: Delay init because locked.") + // If the device is still encrypted and locked do not initialize anything that + // may potentially need the encrypted app storage like databases. + return + } + + synchronized(initLock) { + init() + initialized = true } + } + + fun onBootUnlocked() { + synchronized(initLock) { + if (!initialized) { + init() + } + initialized = true + } + } + + private fun init() { + Log.i(tag, "KeyMapperApp: Init") ServiceLocator.settingsRepository(this).get(Keys.darkTheme) .map { it?.toIntOrNull() } 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 d77967ed77..8c3abdb624 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 @@ -777,7 +777,8 @@ fun ActionData.canBeHeldDown(): Boolean = when (this) { fun ActionData.canUseImeToPerform(): Boolean = when (this) { is ActionData.InputKeyEvent -> !useShell - is ActionData.Text -> true + // Android 13+ can use the accessibility service to input text. + is ActionData.Text -> Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU is ActionData.MoveCursorToEnd -> true else -> false } 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 d97e5a9df0..5313492065 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 @@ -333,7 +333,11 @@ class PerformActionsUseCaseImpl( } is ActionData.Text -> { - imeInputEventInjector.inputText(action.text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + accessibilityService.inputText(action.text) + } else { + imeInputEventInjector.inputText(action.text) + } result = Success(Unit) } 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 4d0a584670..7b9abecc16 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 @@ -476,7 +476,8 @@ class BackupManagerImpl( modifiedGroup = RepositoryUtils.saveUniqueName( modifiedGroup, saveBlock = { renamedGroup -> - if (siblings.any { sibling -> sibling.name == renamedGroup.name }) { + // Do not rename the group with a (1) if it is the same UID. Just overwrite the name. + if (siblings.any { sibling -> sibling.uid != renamedGroup.uid && sibling.name == renamedGroup.name }) { throw IllegalStateException("Non unique group name") } }, diff --git a/app/src/main/java/io/github/sds100/keymapper/groups/GroupRow.kt b/app/src/main/java/io/github/sds100/keymapper/groups/GroupRow.kt index a9f112206a..49f36f5762 100644 --- a/app/src/main/java/io/github/sds100/keymapper/groups/GroupRow.kt +++ b/app/src/main/java/io/github/sds100/keymapper/groups/GroupRow.kt @@ -51,6 +51,7 @@ import io.github.sds100.keymapper.util.ui.compose.ComposeIconInfo fun GroupRow( modifier: Modifier = Modifier, groups: List, + showNewGroup: Boolean = true, onNewGroupClick: () -> Unit = {}, onGroupClick: (String) -> Unit = {}, enabled: Boolean = true, @@ -80,19 +81,21 @@ fun GroupRow( // Show new group button in the expand indicator if the new group button // in the flow row has overflowed. Row { - NewGroupButton( - onClick = onNewGroupClick, - text = if (isSubgroups) { - stringResource(R.string.home_new_subgroup_button) - } else { - stringResource(R.string.home_new_group_button) - }, - icon = { - Icon(imageVector = Icons.Rounded.Add, null) - }, - showText = groups.isEmpty(), - enabled = enabled, - ) + if (showNewGroup) { + NewGroupButton( + onClick = onNewGroupClick, + text = if (isSubgroups) { + stringResource(R.string.home_new_subgroup_button) + } else { + stringResource(R.string.home_new_group_button) + }, + icon = { + Icon(imageVector = Icons.Rounded.Add, null) + }, + showText = groups.isEmpty(), + enabled = enabled, + ) + } Spacer(Modifier.width(8.dp)) @@ -160,19 +163,21 @@ fun GroupRow( ) } - NewGroupButton( - onClick = onNewGroupClick, - text = if (isSubgroups) { - stringResource(R.string.home_new_subgroup_button) - } else { - stringResource(R.string.home_new_group_button) - }, - icon = { - Icon(imageVector = Icons.Rounded.Add, null) - }, - showText = groups.isEmpty(), - enabled = enabled, - ) + if (showNewGroup) { + NewGroupButton( + onClick = onNewGroupClick, + text = if (isSubgroups) { + stringResource(R.string.home_new_subgroup_button) + } else { + stringResource(R.string.home_new_group_button) + }, + icon = { + Icon(imageVector = Icons.Rounded.Add, null) + }, + showText = groups.isEmpty(), + enabled = enabled, + ) + } } } } 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 453c9caed9..0f955052e4 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 @@ -41,6 +41,7 @@ import androidx.compose.ui.layout.onSizeChanged 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 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -176,7 +177,7 @@ fun HomeKeyMapListScreen( ) }, appBarContent = { - KeyMapAppBar( + KeyMapListAppBar( state = state.appBarState, scrollBehavior = scrollBehavior, onSettingsClick = onSettingsClick, @@ -499,7 +500,7 @@ private fun PreviewSelectingKeyMaps() { ) }, appBarContent = { - KeyMapAppBar(state = appBarState) + KeyMapListAppBar(state = appBarState) }, selectionBottomSheet = { SelectionBottomSheet( @@ -542,7 +543,7 @@ private fun PreviewKeyMapsRunning() { ) }, appBarContent = { - KeyMapAppBar(state = appBarState) + KeyMapListAppBar(state = appBarState) }, selectionBottomSheet = {}, ) @@ -578,7 +579,7 @@ private fun PreviewKeyMapsPaused() { ) }, appBarContent = { - KeyMapAppBar(state = appBarState) + KeyMapListAppBar(state = appBarState) }, selectionBottomSheet = {}, ) @@ -633,7 +634,7 @@ private fun PreviewKeyMapsWarnings() { ) }, appBarContent = { - KeyMapAppBar(state = appBarState) + KeyMapListAppBar(state = appBarState) }, selectionBottomSheet = {}, ) @@ -641,7 +642,7 @@ private fun PreviewKeyMapsWarnings() { } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview(device = Devices.PIXEL) @Composable private fun PreviewKeyMapsWarningsEmpty() { val warnings = listOf( @@ -680,7 +681,7 @@ private fun PreviewKeyMapsWarningsEmpty() { ) }, appBarContent = { - KeyMapAppBar(state = appBarState) + KeyMapListAppBar(state = appBarState) }, selectionBottomSheet = {}, ) diff --git a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapAppBar.kt b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt similarity index 97% rename from app/src/main/java/io/github/sds100/keymapper/home/KeyMapAppBar.kt rename to app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt index d952ec3ae4..12555778e4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/KeyMapAppBar.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/KeyMapListAppBar.kt @@ -111,7 +111,7 @@ import kotlinx.coroutines.launch @Composable @OptIn(ExperimentalMaterial3Api::class) -fun KeyMapAppBar( +fun KeyMapListAppBar( modifier: Modifier = Modifier, state: KeyMapAppBarState, onSettingsClick: () -> Unit = {}, @@ -146,10 +146,17 @@ fun KeyMapAppBar( state = state, scrollBehavior = scrollBehavior, onTogglePausedClick = onTogglePausedClick, - onSortClick = onSortClick, onFixWarningClick = onFixWarningClick, onNewGroupClick = onNewGroupClick, onGroupClick = onGroupClick, + navigationIcon = { + IconButton(onClick = onSortClick) { + Icon( + Icons.AutoMirrored.Rounded.Sort, + contentDescription = stringResource(R.string.home_app_bar_sort), + ) + } + }, actions = { AppBarActions( onHelpClick, @@ -185,16 +192,18 @@ fun KeyMapAppBar( LaunchedEffect(state.groupName) { showDeleteGroupDialog = false error = null + val endPosition = state.groupName.length if (state.isEditingGroupName) { if (state.isNewGroup) { newName = TextFieldValue() } else { - val endPosition = state.groupName.length - newName = TextFieldValue(state.groupName, selection = TextRange(endPosition)) } + } else { + newName = + TextFieldValue(state.groupName, selection = TextRange(endPosition)) } } @@ -280,10 +289,10 @@ private fun RootGroupAppBar( state: KeyMapAppBarState.RootGroup, scrollBehavior: TopAppBarScrollBehavior, onTogglePausedClick: () -> Unit, - onSortClick: () -> Unit, onFixWarningClick: (String) -> Unit, onNewGroupClick: () -> Unit, onGroupClick: (String) -> Unit, + navigationIcon: @Composable () -> Unit, actions: @Composable RowScope.() -> Unit, ) { // This is taken from the AppBar color code. @@ -317,14 +326,7 @@ private fun RootGroupAppBar( onTogglePausedClick = onTogglePausedClick, ) }, - navigationIcon = { - IconButton(onClick = onSortClick) { - Icon( - Icons.AutoMirrored.Rounded.Sort, - contentDescription = stringResource(R.string.home_app_bar_sort), - ) - } - }, + navigationIcon = navigationIcon, actions = actions, colors = appBarColors, ) @@ -912,7 +914,7 @@ private fun KeyMapsChildGroupPreview() { isNewGroup = false, ) KeyMapperTheme { - KeyMapAppBar(modifier = Modifier.fillMaxWidth(), state = state) + KeyMapListAppBar(modifier = Modifier.fillMaxWidth(), state = state) } } @@ -931,7 +933,7 @@ private fun KeyMapsChildGroupDarkPreview() { isNewGroup = false, ) KeyMapperTheme(darkTheme = true) { - KeyMapAppBar(modifier = Modifier.fillMaxWidth(), state = state) + KeyMapListAppBar(modifier = Modifier.fillMaxWidth(), state = state) } } @@ -982,7 +984,7 @@ private fun KeyMapsChildGroupEditingDarkPreview() { } KeyMapperTheme(darkTheme = true) { - KeyMapAppBar( + KeyMapListAppBar( state = state, ) } @@ -1022,7 +1024,7 @@ private fun KeyMapsRunningPreview() { isPaused = false, ) KeyMapperTheme { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } @@ -1036,7 +1038,7 @@ private fun HomeStatePausedPreview() { isPaused = true, ) KeyMapperTheme { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } @@ -1062,7 +1064,7 @@ private fun HomeStateWarningsPreview() { isPaused = true, ) KeyMapperTheme { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } @@ -1088,7 +1090,7 @@ private fun HomeStateWarningsDarkPreview() { isPaused = true, ) KeyMapperTheme(darkTheme = true) { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } @@ -1105,7 +1107,7 @@ private fun HomeStateSelectingPreview() { showThisGroup = false, ) KeyMapperTheme { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } @@ -1122,6 +1124,6 @@ private fun HomeStateSelectingDisabledPreview() { showThisGroup = false, ) KeyMapperTheme { - KeyMapAppBar(state = state) + KeyMapListAppBar(state = state) } } 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 b37c03f323..9c5b0c9390 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 @@ -148,7 +148,7 @@ private fun ConfigKeyMapScreen( modifier.displayCutoutPadding(), snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { - KeyMapAppBar( + ConfigKeyMapAppBar( isKeyMapEnabled = isKeyMapEnabled, onKeyMapEnabledChange = onKeyMapEnabledChange, onBackClick = onBackClick, @@ -163,7 +163,7 @@ private fun ConfigKeyMapScreen( ConfigKeyMapTab.ACTIONS -> actionsHelpUrl ConfigKeyMapTab.CONSTRAINTS -> constraintsHelpUrl ConfigKeyMapTab.OPTIONS -> optionsHelpUrl - else -> return@KeyMapAppBar + else -> return@ConfigKeyMapAppBar } if (url.isNotEmpty()) { @@ -298,7 +298,7 @@ private fun ConfigKeyMapScreen( } @Composable -private fun KeyMapAppBar( +private fun ConfigKeyMapAppBar( modifier: Modifier = Modifier, isKeyMapEnabled: Boolean, onKeyMapEnabledChange: (Boolean) -> Unit = {}, diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt index 07c63bd78e..ae124afada 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutScreen.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.mappings.keymaps import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -9,14 +10,19 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.FlashlightOn +import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.AlertDialog -import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -27,12 +33,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.R import io.github.sds100.keymapper.compose.KeyMapperTheme import io.github.sds100.keymapper.constraints.ConstraintMode +import io.github.sds100.keymapper.groups.GroupBreadcrumbRow +import io.github.sds100.keymapper.groups.GroupListItemModel +import io.github.sds100.keymapper.groups.GroupRow import io.github.sds100.keymapper.mappings.keymaps.trigger.KeyMapListItemModel import io.github.sds100.keymapper.mappings.keymaps.trigger.TriggerError import io.github.sds100.keymapper.util.Error @@ -52,24 +63,28 @@ fun CreateKeyMapShortcutScreen( CreateKeyMapShortcutScreen( modifier = modifier, - listItems = state.listItems, + state = state, showShortcutNameDialog = viewModel.showShortcutNameDialog, dismissShortcutNameDialog = { viewModel.showShortcutNameDialog = null }, onShortcutNameResult = { name -> viewModel.shortcutNameDialogResult.value = name viewModel.showShortcutNameDialog = null }, - onClickKeyMap = viewModel::onKeyMapCardClick, + onKeyMapClick = viewModel::onKeyMapCardClick, + onGroupClick = viewModel::onGroupClick, + onPopGroupClick = viewModel::onPopGroupClick, finishActivity = finishActivity, - ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun CreateKeyMapShortcutScreen( modifier: Modifier = Modifier, - listItems: State>, - onClickKeyMap: (String) -> Unit = {}, + state: KeyMapListState, + onKeyMapClick: (String) -> Unit = {}, + onGroupClick: (String?) -> Unit = {}, + onPopGroupClick: () -> Unit = {}, finishActivity: () -> Unit = {}, showShortcutNameDialog: String?, dismissShortcutNameDialog: () -> Unit = {}, @@ -92,41 +107,101 @@ private fun CreateKeyMapShortcutScreen( ) } - // TODO allow navigating between groups and hide the FAB. + BackHandler { showBackDialog = true } + Scaffold( modifier = modifier, - bottomBar = { - BottomAppBar( - actions = { - IconButton(onClick = { showBackDialog = true }) { - Icon( - Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = stringResource(R.string.bottom_app_bar_back_content_description), - ) + topBar = { + AnimatedContent(state.appBarState, contentKey = { it::class }) { state -> + when (state) { + is KeyMapAppBarState.RootGroup -> + Column(modifier) { + CenterAlignedTopAppBar( + title = { + Text(stringResource(R.string.create_key_map_shortcut_app_title)) + }, + navigationIcon = { + IconButton(onClick = { showBackDialog = true }) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.bottom_app_bar_back_content_description), + ) + } + }, + ) + + GroupRow( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + groups = state.subGroups, + showNewGroup = false, + onGroupClick = onGroupClick, + isSubgroups = false, + ) + } + + is KeyMapAppBarState.ChildGroup -> { + Column { + TopAppBar( + title = { + Text( + state.groupName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton(onClick = onPopGroupClick) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.home_app_bar_pop_group), + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) + + GroupBreadcrumbRow( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + groups = state.breadcrumbs, + onGroupClick = onGroupClick, + ) + + GroupRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + groups = state.subGroups, + showNewGroup = false, + onGroupClick = onGroupClick, + enabled = true, + isSubgroups = true, + ) + } } - }, - ) + + else -> {} + } + } }, ) { contentPadding -> Column(modifier = Modifier.padding(contentPadding)) { - Text( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.caption_create_keymap_shortcut), - ) - KeyMapList( modifier = Modifier.fillMaxSize(), footerText = stringResource(R.string.create_key_map_shortcut_footer), - listItems = listItems, + listItems = state.listItems, isSelectable = false, - onClickKeyMap = onClickKeyMap, + onClickKeyMap = onKeyMapClick, ) } } - - BackHandler { - showBackDialog = true - } } @Composable @@ -193,7 +268,7 @@ private fun ShortcutNameDialog( } @Composable -private fun sampleList(): List { +private fun keyMapSampleList(): List { val context = LocalContext.current return listOf( @@ -260,12 +335,106 @@ private fun sampleList(): List { ) } +@Composable +private fun constraintsSampleList(): List { + val ctx = LocalContext.current + + return listOf( + ComposeChipModel.Normal( + id = "1", + text = "Device is locked", + icon = ComposeIconInfo.Vector(Icons.Outlined.Lock), + ), + ComposeChipModel.Normal( + id = "2", + text = "Key Mapper is open", + icon = ComposeIconInfo.Drawable(ctx.drawable(R.mipmap.ic_launcher_round)), + ), + ComposeChipModel.Error( + id = "2", + text = "Key Mapper not found", + error = Error.AppNotFound(Constants.PACKAGE_NAME), + ), + ) +} + +@Composable +private fun groupSampleList(): List { + val ctx = LocalContext.current + + return listOf( + GroupListItemModel( + uid = "1", + name = "Lockscreen", + icon = ComposeIconInfo.Vector(Icons.Outlined.Lock), + ), + GroupListItemModel( + uid = "2", + name = "Key Mapper", + icon = ComposeIconInfo.Drawable(ctx.drawable(R.mipmap.ic_launcher_round)), + ), + GroupListItemModel( + uid = "3", + name = "Key Mapper", + icon = null, + ), + ) +} + +@Preview +@Composable +private fun PreviewRootGroup() { + KeyMapperTheme { + CreateKeyMapShortcutScreen( + state = KeyMapListState( + appBarState = KeyMapAppBarState.RootGroup( + subGroups = groupSampleList(), + warnings = emptyList(), + isPaused = true, + ), + listItems = State.Data(keyMapSampleList()), + ), + showShortcutNameDialog = null, + ) + } +} + +@Preview +@Composable +private fun PreviewChildGroup() { + KeyMapperTheme { + CreateKeyMapShortcutScreen( + state = KeyMapListState( + appBarState = KeyMapAppBarState.ChildGroup( + groupName = "Very very very very very long name", + subGroups = groupSampleList(), + constraints = constraintsSampleList(), + parentConstraintCount = 1, + constraintMode = ConstraintMode.AND, + breadcrumbs = groupSampleList(), + isEditingGroupName = false, + isNewGroup = false, + ), + listItems = State.Data(keyMapSampleList()), + ), + showShortcutNameDialog = null, + ) + } +} + @Preview @Composable -private fun Preview() { +private fun PreviewEmpty() { KeyMapperTheme { CreateKeyMapShortcutScreen( - listItems = State.Data(sampleList()), + state = KeyMapListState( + appBarState = KeyMapAppBarState.RootGroup( + subGroups = emptyList(), + warnings = emptyList(), + isPaused = true, + ), + listItems = State.Data(emptyList()), + ), showShortcutNameDialog = null, ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt index 102e6582f0..50cde0b7a6 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/CreateKeyMapShortcutViewModel.kt @@ -122,7 +122,7 @@ class CreateKeyMapShortcutViewModel( ) } - val parentGroupListItems = keyMapGroup.parents.map { group -> + val breadcrumbs = keyMapGroup.parents.plus(keyMapGroup.group).filterNotNull().map { group -> GroupListItemModel( uid = group.uid, name = group.name, @@ -142,7 +142,7 @@ class CreateKeyMapShortcutViewModel( subGroups = subGroupListItems, constraints = emptyList(), constraintMode = ConstraintMode.AND, - breadcrumbs = parentGroupListItems, + breadcrumbs = breadcrumbs, isEditingGroupName = false, isNewGroup = false, parentConstraintCount = keyMapGroup.parents.sumOf { it.constraintState.constraints.size }, @@ -220,6 +220,18 @@ class CreateKeyMapShortcutViewModel( } } + fun onGroupClick(uid: String?) { + viewModelScope.launch { + listKeyMaps.openGroup(uid) + } + } + + fun onPopGroupClick() { + viewModelScope.launch { + listKeyMaps.popGroup() + } + } + class Factory( private val configKeyMapUseCase: ConfigKeyMapUseCase, private val listUseCase: ListKeyMapsUseCase, 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 36bfa61983..71d155010d 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 @@ -90,7 +90,9 @@ fun KeyMapList( is State.Data -> { Surface(modifier = modifier) { if (listItems.data.isEmpty()) { - EmptyKeyMapList(modifier = Modifier.fillMaxSize()) + EmptyKeyMapList( + modifier = Modifier.fillMaxSize().padding(bottom = bottomListPadding), + ) } else { LoadedKeyMapList( Modifier.fillMaxSize(), 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 318b40f42b..4352967f2b 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 @@ -539,7 +539,9 @@ class KeyMapListViewModel( } fun onEnableGuiKeyboardClick() { - setupGuiKeyboard.enableInputMethod() + coroutineScope.launch { + setupGuiKeyboard.enableInputMethod() + } } fun onChooseGuiKeyboardClick() { 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 7116cf1550..847da51c4b 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 @@ -754,7 +754,9 @@ abstract class BaseConfigTriggerViewModel( } fun onEnableGuiKeyboardClick() { - setupGuiKeyboard.enableInputMethod() + coroutineScope.launch { + setupGuiKeyboard.enableInputMethod() + } } fun onChooseGuiKeyboardClick() { diff --git a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt index 9e08992011..c891d88b8a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/mappings/keymaps/trigger/SetupGuiKeyboardUseCase.kt @@ -31,7 +31,7 @@ class SetupGuiKeyboardUseCaseImpl( } } - override fun enableInputMethod() { + override suspend fun enableInputMethod() { inputMethodAdapter.getInfoByPackageName(KeyMapperImeHelper.KEY_MAPPER_GUI_IME_PACKAGE) .onSuccess { inputMethodAdapter.enableIme(it.id) @@ -54,7 +54,7 @@ interface SetupGuiKeyboardUseCase { val isInstalled: Flow val isEnabled: Flow - fun enableInputMethod() + suspend fun enableInputMethod() val isChosen: Flow fun chooseInputMethod() diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt index d817d1e4a8..fbc4707022 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/ConfigSettingsUseCase.kt @@ -81,7 +81,7 @@ class ConfigSettingsUseCaseImpl( override val connectedInputDevices: StateFlow>> get() = devicesAdapter.connectedInputDevices - override fun enableCompatibleIme() { + override suspend fun enableCompatibleIme() { imeHelper.enableCompatibleInputMethods() } @@ -190,7 +190,7 @@ interface ConfigSettingsUseCase { val rerouteKeyEvents: Flow val isCompatibleImeChosen: Flow val isCompatibleImeEnabled: Flow - fun enableCompatibleIme() + suspend fun enableCompatibleIme() suspend fun chooseCompatibleIme(): Result suspend fun showImePicker(): Result<*> diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt index f1946aad5a..5c887d835e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/MainSettingsFragment.kt @@ -11,7 +11,6 @@ import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.findNavController import androidx.preference.DropDownPreference -import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreferenceCompat @@ -135,9 +134,9 @@ class MainSettingsFragment : BaseSettingsFragment() { isSingleLineTitle = false setTitle(R.string.title_pref_dark_theme) + setSummary(R.string.summary_pref_dark_theme) entries = strArray(R.array.pref_dark_theme_entries) entryValues = ThemeUtils.THEMES.map { it.toString() }.toTypedArray() - summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() addPreference(this) } diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt index 8bb9c59352..6c96e76939 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/SettingsViewModel.kt @@ -140,7 +140,9 @@ class SettingsViewModel( } fun onEnableCompatibleImeClick() { - useCase.enableCompatibleIme() + viewModelScope.launch { + useCase.enableCompatibleIme() + } } fun resetDefaultMappingOptions() { diff --git a/app/src/main/java/io/github/sds100/keymapper/settings/ThemeUtils.kt b/app/src/main/java/io/github/sds100/keymapper/settings/ThemeUtils.kt index e8bbde0f2c..83a6a98994 100644 --- a/app/src/main/java/io/github/sds100/keymapper/settings/ThemeUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/settings/ThemeUtils.kt @@ -9,5 +9,5 @@ object ThemeUtils { const val LIGHT = 1 const val AUTO = 2 - val THEMES = arrayOf(DARK, LIGHT, AUTO) + val THEMES = arrayOf(LIGHT, DARK, AUTO) } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/BootBroadcastReceiver.kt b/app/src/main/java/io/github/sds100/keymapper/system/BootBroadcastReceiver.kt index 57d5be017c..c61aab628b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/BootBroadcastReceiver.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/BootBroadcastReceiver.kt @@ -3,7 +3,7 @@ package io.github.sds100.keymapper.system import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import io.github.sds100.keymapper.ServiceLocator +import io.github.sds100.keymapper.KeyMapperApp /** * Created by sds100 on 24/03/2019. @@ -12,12 +12,9 @@ import io.github.sds100.keymapper.ServiceLocator class BootBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { context ?: return - if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { - /* - Initializing the controller will update any notifications since it will collect the values - in the constructor - */ - ServiceLocator.notificationController(context) + + if (intent?.action == Intent.ACTION_LOCKED_BOOT_COMPLETED) { + (context.applicationContext as? KeyMapperApp)?.onBootUnlocked() } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/BaseAccessibilityServiceController.kt index 6da22df6af..51252b41d7 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,6 +150,10 @@ 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 } @@ -505,6 +509,11 @@ abstract class BaseAccessibilityServiceController( } is ServiceEvent.TriggerKeyMap -> triggerKeyMapFromIntent(event.uid) + + is ServiceEvent.EnableInputMethod -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + accessibilityService.setInputMethodEnabled(event.imeId, true) + } + else -> Unit } } 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 4698e6d4b6..449f83c0fa 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 @@ -49,6 +49,8 @@ interface IAccessibilityService { val rootNode: AccessibilityNodeModel? val activeWindowPackage: Flow + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun setInputMethodEnabled(imeId: String, enabled: Boolean) fun hideKeyboard() fun showKeyboard() val isKeyboardHidden: Flow @@ -59,4 +61,7 @@ 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 b909f9ae6f..54e3538019 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 @@ -4,6 +4,7 @@ import android.accessibilityservice.AccessibilityService import android.accessibilityservice.FingerprintGestureController import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription.StrokeDescription +import android.annotation.SuppressLint import android.app.ActivityManager import android.content.Intent import android.content.res.Configuration @@ -560,4 +561,17 @@ 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) { + softKeyboardController.setInputMethodEnabled(imeId, enabled) + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt index d2a41d3712..ca96844989 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/camera/AndroidCameraAdapter.kt @@ -209,39 +209,21 @@ class AndroidCameraAdapter(context: Context) : CameraAdapter { try { val cameraId = getFlashlightCameraIdForLens(lens) + val flashInfo = getFlashInfo(lens) - if (cameraId == null) { + if (cameraId == null || flashInfo == null) { return when (lens) { CameraLens.FRONT -> Error.FrontFlashNotFound CameraLens.BACK -> Error.BackFlashNotFound } } - val maxStrength = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getCharacteristicForLens( - lens, - CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL, - ) - } else { - null - } - - val defaultStrength = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getCharacteristicForLens( - lens, - CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL, - ) - } else { - null - } - // try to find a camera with a flash - if (enabled && maxStrength != null && defaultStrength != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enabled && flashInfo.supportsVariableStrength) { val strength = if (strengthPercent == null) { - defaultStrength + flashInfo.defaultStrength } else { - (strengthPercent * maxStrength).toInt().coerceAtLeast(1) + (strengthPercent * flashInfo.maxStrength).toInt().coerceAtLeast(1) } cameraManager.turnOnTorchWithStrengthLevel(cameraId, strength) } else { diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt index 4701c96ee4..65b99149e4 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt @@ -13,6 +13,7 @@ import android.provider.Settings import android.view.inputmethod.InputMethodManager import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.system.JobSchedulerHelper import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.accessibility.ServiceAdapter @@ -177,7 +178,7 @@ class AndroidInputMethodAdapter( } } - override fun enableIme(imeId: String): Result<*> = enableImeWithoutUserInput(imeId).otherwise { + override suspend fun enableIme(imeId: String): Result<*> = enableImeWithoutUserInput(imeId).otherwise { try { val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS) intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK @@ -189,7 +190,15 @@ class AndroidInputMethodAdapter( } } - private fun enableImeWithoutUserInput(imeId: String): Result<*> = suAdapter.execute("ime enable $imeId") + private suspend fun enableImeWithoutUserInput(imeId: String): Result<*> { + return getInfoByPackageName(Constants.PACKAGE_NAME).then { keyMapperImeInfo -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && imeId == keyMapperImeInfo.id) { + serviceAdapter.send(ServiceEvent.EnableInputMethod(keyMapperImeInfo.id)) + } else { + suAdapter.execute("ime enable $imeId") + } + } + } override suspend fun chooseImeWithoutUserInput(imeId: String): Result { getInfoById(imeId).onSuccess { @@ -292,7 +301,8 @@ class AndroidInputMethodAdapter( private fun getChosenImeId(): String = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD) private fun getImeId(packageName: String): Result { - val imeId = inputMethodManager.inputMethodList.find { it.packageName == packageName }?.id + val imeId = + inputMethodManager.inputMethodList.find { it.packageName == packageName }?.id return if (imeId == null) { Error.InputMethodNotFound(packageName) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputMethodAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputMethodAdapter.kt index b284f031ab..1b0257b4b3 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputMethodAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputMethodAdapter.kt @@ -11,7 +11,7 @@ interface InputMethodAdapter { val isUserInputRequiredToChangeIme: Flow fun showImePicker(fromForeground: Boolean): Result<*> - fun enableIme(imeId: String): Result<*> + suspend fun enableIme(imeId: String): Result<*> suspend fun chooseImeWithoutUserInput(imeId: String): Result diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt index 3bf0d1f0d0..e62df2ad11 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeHelper.kt @@ -40,7 +40,7 @@ class KeyMapperImeHelper(private val imeAdapter: InputMethodAdapter) { imeAdapter.inputMethods .map { containsCompatibleIme(it) } - fun enableCompatibleInputMethods() { + suspend fun enableCompatibleInputMethods() { KEY_MAPPER_IME_PACKAGE_LIST.forEach { packageName -> imeAdapter.getInfoByPackageName(packageName).onSuccess { imeAdapter.enableIme(it.id) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index 2773a4eddf..20f63ce5a7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -1,13 +1,21 @@ package io.github.sds100.keymapper.system.inputmethod +import android.annotation.SuppressLint +import android.app.KeyguardManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.inputmethodservice.InputMethodService +import android.os.Build +import android.os.UserManager +import android.util.Log import android.view.KeyEvent import android.view.MotionEvent +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import io.github.sds100.keymapper.Constants import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.api.KeyEventRelayService @@ -36,6 +44,15 @@ class KeyMapperImeService : InputMethodService() { "io.github.sds100.keymapper.inputmethod.EXTRA_KEY_EVENT" } + private val userManager: UserManager? by lazy { getSystemService() } + private val inputMethodManager: InputMethodManager? by lazy { + getSystemService() + } + + private val keyguardManager: KeyguardManager? by lazy { + getSystemService() + } + private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val action = intent?.action ?: return @@ -120,6 +137,23 @@ class KeyMapperImeService : InputMethodService() { keyEventRelayServiceWrapper.onCreate() } + override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) { + super.onStartInput(attribute, restarting) + + // IMPORTANT! Select a keyboard with an actual GUI if the user needs + // to unlock their device. This must not be in onCreate because + // the switchInputMethod does not work there. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && userManager?.isUserUnlocked == false) { + selectNonBasicKeyboard() + } else if ( + !restarting && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 && + keyguardManager?.isDeviceLocked == true + ) { + selectNonBasicKeyboard() + } + } + override fun onGenericMotionEvent(event: MotionEvent?): Boolean { event ?: return super.onGenericMotionEvent(null) @@ -174,4 +208,28 @@ class KeyMapperImeService : InputMethodService() { super.onDestroy() } + + @SuppressLint("LogNotTimber") + private fun selectNonBasicKeyboard() { + inputMethodManager ?: return + + inputMethodManager!!.enabledInputMethodList + .filter { + it.packageName != "io.github.sds100.keymapper" && + it.packageName != "io.github.sds100.keymapper.debug" && + it.packageName != "io.github.sds100.keymapper.ci" + } + // Select a random one in case one of them can't be used on the lock screen such as + // the Google Voice Typing keyboard. This is critical because i + // f an input method can't be used + // then it will select the Key Mapper Basic Input method again and loop forever. + .randomOrNull() + ?.also { + Log.e( + KeyMapperImeService::class.simpleName, + "Device is locked! Select ${it.id} input method", + ) + switchInputMethod(it.id) + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt index 4db23d48bb..054dd384fa 100644 --- a/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt +++ b/app/src/main/java/io/github/sds100/keymapper/util/ServiceEvent.kt @@ -69,4 +69,7 @@ sealed class ServiceEvent { @Serializable data class TriggerKeyMap(val uid: String) : ServiceEvent() + + @Serializable + data class EnableInputMethod(val imeId: String) : ServiceEvent() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d6ba52057..47df1d9510 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,8 +55,8 @@ You can delete saved sound files in the settings. Can\'t find any paired devices. Is Bluetooth turned on? - The \"Allow other apps to trigger this key map\" option will be turned on for the key map that you select if it isn\'t already. If you turn this option off later then any shortcuts or Intents to trigger this key map will not work. Tap a key map to use as a shortcut. + Create key map shortcut Enabled Disabled @@ -621,7 +621,8 @@ Enable this if you want to use features/actions which only work on rooted devices. Key Mapper must have root permission from your root-access-management app (e.g Magisk, SuperSU) for these features to work. Only turn this on if you know your device is rooted and you have given Key Mapper root permission. - Dark theme + Choose theme + Light and dark themes available Switch between the Key Mapper keyboard and your default keyboard when you tap the notification. Toggle Key Mapper keyboard notification @@ -708,9 +709,9 @@ - @string/on - @string/off - @string/follow_system + Light + Dark + Follow system diff --git a/app/version.properties b/app/version.properties index ce8caecb21..993f2b53ec 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.0.0-beta.3 -VERSION_CODE=90 +VERSION_NAME=3.0.0-beta.4 +VERSION_CODE=91 VERSION_NUM=0 \ No newline at end of file