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 3efa21ed49..ff3b000cd1 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 @@ -336,5 +336,15 @@ sealed interface Action { override val permissions: List = listOf("SHIZUKU", "ROOT") override val isConfigurable: Boolean = true } + + @Keep + data class Keyboard( + @SerializedName("packageName") val packageName: String? = null + ) : Action { + override val title: Int = R.string.feat_system_keyboard_title + override val icon: Int = R.drawable.rounded_keyboard_24 + override val permissions: List = listOf("WRITE_SECURE_SETTINGS") + override val isConfigurable: Boolean = true + } } 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 836baa5b06..ef8226ec88 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 43ea619f66..b6bd7ac43c 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 @@ -16,12 +16,15 @@ import android.hardware.camera2.CameraManager import android.media.AudioManager import android.net.wifi.WifiManager import android.os.Build +import android.provider.Settings +import android.util.Log import android.view.KeyEvent import android.widget.Toast 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.performHapticFeedback import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper @@ -650,6 +653,18 @@ 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.packageName) + return@withContext + } + Toast.makeText(context, "give me Write Secure Settings permission to change your keyboard.", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Keyboard Switching Failed: ${e.message ?: ""}", Toast.LENGTH_SHORT).show() + } + } } } 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 4469dd4f83..e3273cac06 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 @@ -101,6 +102,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 @@ -265,6 +267,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 configAction by remember { mutableStateOf(null) } // Generic config action val isTriggerConfigured = when (val trigger = selectedTrigger) { @@ -902,7 +906,8 @@ class AutomationEditorActivity : ComponentActivity() { Action.FreezeApps(), Action.UnfreezeApps(), Action.FreezeTag(), - Action.PinApp + Action.PinApp, + Action.Keyboard() ) val systemActions = listOf( Action.TurnOnFlashlight, @@ -957,6 +962,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, @@ -971,14 +983,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 @@ -996,6 +1008,10 @@ class AutomationEditorActivity : ComponentActivity() { temporarySelectedAppsForAction = resolvedAction.packageNames showFreezeAppsSettings = true } + is Action.Keyboard -> { + showSetKeyboardSheet = true + selectedIme = resolvedAction.packageName + } else -> {} } } @@ -1279,6 +1295,26 @@ class AutomationEditorActivity : ComponentActivity() { excludePackages = if (automationType == Automation.Type.APP) selectedApps else emptyList() ) } + + 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 + ) + } } if (showPermissionSheet) { 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 0000000000..5d9b771d84 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/KeyboardSelectionSheet.kt @@ -0,0 +1,194 @@ +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.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +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.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.painterResource +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 com.sameerasw.essentials.utils.PermissionUtils +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 keyboards list: ${e.message ?: ""}" + ) + } finally { + isLoadingKeyboards = false + } + } + + EssentialsBottomSheet( + onDismissRequest = { onDismissRequest(defaultInputMethod) }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Select Keyboard", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + + if (PermissionUtils.canWriteSecureSettings(context)) { + Icon( + painter = painterResource(R.drawable.rounded_shield_lock_24), + contentDescription = "Permission", + tint = MaterialTheme.colorScheme.primary + ) + } + } + + if (isLoadingKeyboards) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + horizontalArrangement = Arrangement.Center + ) { + LoadingIndicator() + } + } else { + LazyColumn( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(24.dp)), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + itemsIndexed(imesList, key = { _, ime -> ime.id }) { index, ime -> + val isEnabled = ime.serviceInfo.enabled + val isSelected = defaultInputMethod == ime.id + + val currentIndex = + if (imesList.size == 1) -2 else if (index == (imesList.size - 1)) -1 else index + val shape = when (currentIndex) { + -2 -> RoundedCornerShape(24.dp) + -1 -> RoundedCornerShape(4.dp, 4.dp, 24.dp, 24.dp) + 0 -> RoundedCornerShape(24.dp, 24.dp, 4.dp, 4.dp) + else -> RoundedCornerShape(4.dp) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .clickable { + HapticUtil.performUIHaptic(view) + if (isEnabled) { + defaultInputMethod = ime.id + return@clickable + } + Toast.makeText( + context, + "Enable in settings", + Toast.LENGTH_SHORT + ).show() + } + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Image( + bitmap = ime.loadIcon(context.packageManager).toBitmap() + .asImageBitmap(), + contentDescription = "IME Logo", + modifier = Modifier.size(24.dp), + contentScale = ContentScale.Fit + ) + Text( + text = ime.loadLabel(context.packageManager).toString(), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + RadioButton( + selected = isSelected, + onClick = { defaultInputMethod = ime.id }, + enabled = isEnabled + ) + } + } + } + } + } + } +} 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 9bd3db5e72..77cd1e7c7c 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,10 @@ class DIYViewModel(application: Application) : AndroidViewModel(application) { tagIds = suggestion.freezeTagIds ) + "Keyboard" -> Action.Keyboard( + packageName = suggestion.keyboard + ) + else -> null } }