diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index 1c54598ae..4125db242 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -337,6 +337,16 @@ sealed interface Action { override val isConfigurable: Boolean = true } + @Keep + data class Keyboard( + @SerializedName("inputMethodId") val inputMethodId: String? = null + ) : Action { + override val title: Int = R.string.diy_set_keyboard_title + override val icon: Int = R.drawable.rounded_keyboard_24 + override val permissions: List = listOf("WRITE_SECURE_SETTINGS") + override val isConfigurable: Boolean = true + } + @Keep enum class SettingsTable { @SerializedName("SYSTEM") diff --git a/app/src/main/java/com/sameerasw/essentials/domain/genai/AutomationSuggestion.kt b/app/src/main/java/com/sameerasw/essentials/domain/genai/AutomationSuggestion.kt index 836baa5b0..ef8226ec8 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/genai/AutomationSuggestion.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/genai/AutomationSuggestion.kt @@ -105,5 +105,8 @@ data class AutomationSuggestion( val smartPixelsEnabled: Boolean? = null, @Guide(description = "Dim wallpaper amount (0.0 to 1.0) for DimWallpaper action") - val dimWallpaperAmount: Float? = null + val dimWallpaperAmount: Float? = null, + + @Guide(description = "Select keyboard from the list to switch") + val keyboard: String? = null, ) diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index fc937c867..38c189771 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -19,10 +19,12 @@ import android.os.Build import android.provider.Settings import android.view.KeyEvent import android.widget.Toast +import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.HapticFeedbackType import com.sameerasw.essentials.domain.diy.Action import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService import com.sameerasw.essentials.utils.DeviceLockUtils +import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.utils.ShellUtils import com.sameerasw.essentials.utils.performHapticFeedback import rikka.shizuku.ShizukuBinderWrapper @@ -652,6 +654,19 @@ object CombinedActionExecutor { com.sameerasw.essentials.utils.FreezeManager.unfreezeApp(context, pkg) } } + + is Action.Keyboard -> { + try { + if (PermissionUtils.canWriteSecureSettings(context)) { + Settings.Secure.putString(context.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD, action.inputMethodId) + return@withContext + } + Toast.makeText(context, R.string.diy_set_keyboard_permission_required, Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Keyboard Switching Failed: ${e.message ?: ""}", Toast.LENGTH_SHORT).show() + } + } + is Action.CustomSettings -> { val resolver = context.contentResolver for (entry in action.entries) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index 50ec80554..5968bbe63 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -12,6 +12,7 @@ package com.sameerasw.essentials.ui.activities import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.inputmethod.InputMethodInfo import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -102,6 +103,7 @@ import com.sameerasw.essentials.ui.core.sheets.ScreenOffSettingsSheet import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet import com.sameerasw.essentials.ui.core.sheets.SoundModeSettingsSheet import com.sameerasw.essentials.ui.core.sheets.WifiNetworkSelectionSheet +import com.sameerasw.essentials.ui.features.apps.sheets.KeyboardSelectionSheet import com.sameerasw.essentials.ui.theme.EssentialsTheme import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.HapticUtil @@ -266,6 +268,8 @@ class AutomationEditorActivity : ComponentActivity() { var showTimeSettings by remember { mutableStateOf(false) } var showBluetoothSettings by remember { mutableStateOf(false) } var showWifiSettings by remember { mutableStateOf(false) } + var showSetKeyboardSheet by remember { mutableStateOf(false) } + var selectedIme by remember { mutableStateOf(null) } var showCustomSettingsSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action @@ -298,6 +302,7 @@ class AutomationEditorActivity : ComponentActivity() { fun isActionConfigured(action: Action?): Boolean = when (action) { is Action.OpenApp -> action.packageName.isNotBlank() is Action.CustomSettings -> action.entries.isNotEmpty() + is Action.Keyboard -> !action.inputMethodId.isNullOrEmpty() else -> true } @@ -905,7 +910,8 @@ class AutomationEditorActivity : ComponentActivity() { Action.FreezeApps(), Action.UnfreezeApps(), Action.FreezeTag(), - Action.PinApp + Action.PinApp, + Action.Keyboard() ) val systemActions = listOf( Action.TurnOnFlashlight, @@ -961,6 +967,13 @@ class AutomationEditorActivity : ComponentActivity() { ) { actions.forEach { action -> val resolvedAction = if (currentSelection != null && currentSelection::class == action::class) currentSelection else action + val missing = getMissingPermissionsHelper(resolvedAction) + fun showPermissionSheet() { + permissionKeysToShow = missing + permissionFeatureTitle = resolvedAction.title + showPermissionSheet = true + } + EditorActionItem( title = stringResource(resolvedAction.title), iconRes = resolvedAction.icon, @@ -975,14 +988,14 @@ class AutomationEditorActivity : ComponentActivity() { else selectedOutAction = resolvedAction } } - val missing = getMissingPermissionsHelper(resolvedAction) - if (missing.isNotEmpty()) { - permissionKeysToShow = missing - permissionFeatureTitle = resolvedAction.title - showPermissionSheet = true - } + if(missing.isNotEmpty()) showPermissionSheet() }, onSettingsClick = { + if(missing.isNotEmpty()) { + showPermissionSheet() + return@EditorActionItem + } + configAction = resolvedAction when (resolvedAction) { is Action.DimWallpaper -> showDimSettings = true @@ -1000,6 +1013,10 @@ class AutomationEditorActivity : ComponentActivity() { temporarySelectedAppsForAction = resolvedAction.packageNames showFreezeAppsSettings = true } + is Action.Keyboard -> { + showSetKeyboardSheet = true + selectedIme = resolvedAction.inputMethodId + } is Action.CustomSettings -> showCustomSettingsSettings = true else -> {} } @@ -1285,6 +1302,26 @@ class AutomationEditorActivity : ComponentActivity() { ) } + if (showSetKeyboardSheet && configAction is Action.Keyboard) { + KeyboardSelectionSheet( + onDismissRequest = { newIme -> + showSetKeyboardSheet = false + when (automationType) { + Automation.Type.TRIGGER -> selectedAction = Action.Keyboard(newIme) + Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction = + Action.Keyboard(newIme) + + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = Action.Keyboard(newIme) + else selectedOutAction = Action.Keyboard(newIme) + } + } + configAction = null + }, + selectedIme = (configAction as? Action.Keyboard)?.inputMethodId + ) + } + if (showCustomSettingsSettings && configAction is Action.CustomSettings) { CustomSettingsSheet( initialAction = configAction as Action.CustomSettings, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt index f1a5c22f8..96e8e18e0 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt @@ -374,12 +374,12 @@ fun WelcomeStepContent( modifier = Modifier .clip(RoundedCornerShape(100.dp)) .background(MaterialTheme.colorScheme.primaryContainer) - .padding(8.dp) .clickable { val websiteUrl = "https://sameerasw.com" val intent = Intent(Intent.ACTION_VIEW, websiteUrl.toUri()) context.startActivity(intent) - }, + } + .padding(8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/KeyboardSelectionSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/KeyboardSelectionSheet.kt new file mode 100644 index 000000000..bb27de6c9 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/KeyboardSelectionSheet.kt @@ -0,0 +1,189 @@ +package com.sameerasw.essentials.ui.features.apps.sheets + +import android.content.Context +import android.os.Build +import android.provider.Settings +import android.util.Log +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun KeyboardSelectionSheet( + onDismissRequest: (ime: String?) -> Unit, + selectedIme: String?, + context: Context = LocalContext.current +) { + var isLoadingKeyboards by remember { mutableStateOf(true) } + var defaultInputMethod by remember { mutableStateOf(null) } + var imesList by remember { mutableStateOf>(emptyList()) } + val view = LocalView.current + val isDeprecated = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + + LaunchedEffect(Unit) { + isLoadingKeyboards = true + try { + val list = withContext(Dispatchers.IO) { + val imes = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val defaultIme = + if (isDeprecated) + imes.currentInputMethodInfo?.id + else + Settings.Secure.getString( + context.contentResolver, + Settings.Secure.DEFAULT_INPUT_METHOD + ) + defaultIme to imes + } + defaultInputMethod = selectedIme ?: (list.first ?: "") + imesList = list.second.inputMethodList + } catch (e: Exception) { + Log.e( + "KeyboardSelectionSheet", "Error loading input methods list: ${e.message ?: ""}" + ) + } finally { + isLoadingKeyboards = false + } + } + + EssentialsBottomSheet( + onDismissRequest = { onDismissRequest(defaultInputMethod) }, + ) { + Column( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.diy_set_keyboard_sheet_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + } + + if (isLoadingKeyboards) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + horizontalArrangement = Arrangement.Center + ) { + LoadingIndicator() + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(24.dp)), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(imesList, key = { it.id }) { ime -> + val isEnabled = ime.serviceInfo.enabled + val isSelected = defaultInputMethod == ime.id + + ListItem( + checked = isSelected, + onCheckedChange = { + if (isEnabled) { + HapticUtil.performVirtualKeyHaptic(view) + defaultInputMethod = ime.id + } else { + HapticUtil.performVirtualKeyHaptic(view) + Toast.makeText( + context, + R.string.diy_set_keyboard_input_method_disabled, + Toast.LENGTH_SHORT + ).show() + } + }, + onLongClick = null, + enabled = isEnabled, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + leadingContent = { + Image( + bitmap = ime.loadIcon(context.packageManager).toBitmap() + .asImageBitmap(), + contentDescription = ime.serviceInfo.name, + modifier = Modifier.size(24.dp), + contentScale = ContentScale.Fit + ) + }, + supportingContent = null, + trailingContent = { + RadioButton( + selected = if (isEnabled) isSelected else false, + onClick = null, + enabled = isEnabled + ) + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + content = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = ime.loadLabel(context.packageManager).toString(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt index 9bd3db5e7..bec2f6aa7 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/DIYViewModel.kt @@ -226,6 +226,8 @@ class DIYViewModel(application: Application) : AndroidViewModel(application) { tagIds = suggestion.freezeTagIds ) + "Keyboard" -> Action.Keyboard(suggestion.keyboard) + else -> null } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bbacdd59e..af5c23995 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1054,6 +1054,11 @@ Setting Key Value + Set Keyboard + Select Keyboard + Enable input method in settings + Grant permission to change your keyboard. + Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids