Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = listOf("WRITE_SECURE_SETTINGS")
override val isConfigurable: Boolean = true
}

@Keep
enum class SettingsTable {
@SerializedName("SYSTEM")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. Missing isActionConfigured Validation for Action.Keyboard

  • Location: app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt
  • Issue:
    isActionConfigured does not validate Action.Keyboard. If a user adds the "Set Keyboard" action without opening the settings sheet, action.inputMethodId remains null. When triggered, it will execute with null, which can clear the user's default keyboard in Android settings or fail.
  • Fix: In the editor's configuration check, ensure action.inputMethodId != null && action.inputMethodId.isNotBlank().

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String?>(null) }
var showCustomSettingsSettings by remember { mutableStateOf(false) }
var configAction by remember { mutableStateOf<Action?>(null) } // Generic config action

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -905,7 +910,8 @@ class AutomationEditorActivity : ComponentActivity() {
Action.FreezeApps(),
Action.UnfreezeApps(),
Action.FreezeTag(),
Action.PinApp
Action.PinApp,
Action.Keyboard()
)
val systemActions = listOf(
Action.TurnOnFlashlight,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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 -> {}
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 High Severity Issues (Must Fix Before Merge)

1. Design System & Component Styling Violations in KeyboardSelectionSheet.kt

  • Location: app/src/main/java/com/sameerasw/essentials/ui/features/apps/sheets/KeyboardSelectionSheet.kt (Lines 96–193)
  • Issues:
    • Manual Shape Logic / Rule Violation: The code uses ad-hoc corner math with temporary comments (// remove it later and replace with official method.):
      val currentIndex = if (imesList.size == 1) -2 else if (index == (imesList.size - 1)) -1 else index
      val shape = when (currentIndex) { ... }
      This violates our design guidelines. It should be wrapped in RoundedCardContainer(spacing = 2.dp, cornerRadius = 24.dp) using the standard index/count shape segmentation or SegmentedListItem.
    • Unnecessary Fullscreen Height: The outer Column uses Modifier.fillMaxSize(), which forces the bottom sheet to take up the full display height even if the user only has 1 or 2 keyboards installed. Use Modifier.fillMaxWidth() with .weight(1f, fill = false) on the list container instead.
    • Bypassed Haptics on RadioButton: RadioButton(selected = isSelected, onClick = { ... }) defines its own onClick that bypasses HapticUtil.performUIHaptic(view), causing inconsistent feedback when tapping the radio button vs tapping the row. Pass onClick = null on the RadioButton and let the row's Modifier.clickable handle the haptic feedback and selection.

Original file line number Diff line number Diff line change
@@ -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<String?>(null) }
var imesList by remember { mutableStateOf<List<InputMethodInfo>>(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
)
}
}
)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ class DIYViewModel(application: Application) : AndroidViewModel(application) {
tagIds = suggestion.freezeTagIds
)

"Keyboard" -> Action.Keyboard(suggestion.keyboard)

else -> null
}
}
Expand Down
Loading
Loading