From 957ff2f1673140f17801f3cd04cf89cc40c2c3ce Mon Sep 17 00:00:00 2001 From: yanmess1ge-dot <257031600+yanmess1ge-dot@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:25:22 +0000 Subject: [PATCH 1/9] Community translations by @yanmess1ge-dot --- app/src/main/res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4c77d0cec..c5d11a0fe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1626,10 +1626,10 @@ Zudem muss die Einstellung unter Umständen mehrfach angewendet werden, damit si Kein automatisches Einfrieren Apps mit diesem Tag werden vor dem automatischen Einfrieren geschützt Tag-Farben im App-Raster anzeigen - Benachrichtigungen synchronisieren - Zum Essentials-Launcher + Benachrichtigungssynchronisierung + Zum Essentials Launcher Anrufe synchronisieren - Anrufe mit deiner Smartwatch synchronisieren und steuern + Anrufe von Ihrer Uhr synchronisieren und steuern Apps auswählen Nur ausgewählte Apps werden mit der Smartwatch synchronisiert From 8ef8baa80ac52af5bd54ce4a834eeb788ce167b6 Mon Sep 17 00:00:00 2001 From: qxmO5 <277465300+qxmO5@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:38:55 +0000 Subject: [PATCH 2/9] Community translations by @qxmO5 --- app/src/main/res/values-ar/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ed7b3c2ef..c5a5fe755 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1582,4 +1582,9 @@ الى مشغل Essentials استبدال شريط البحث استبدال مشغل Pixel الافتراضيsearchbar + وقت الغفوة الافتراضي + الخيار الاول + الخيار الثاني + الخيار الثالث + الخيار الرابع From 30e18dab6a4382bed6ad1af09dfa15eb8f5516e8 Mon Sep 17 00:00:00 2001 From: qxmO5 <277465300+qxmO5@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:07:15 +0000 Subject: [PATCH 3/9] Community translations by @qxmO5 --- app/src/main/res/values-ar/strings.xml | 65 +++++++++++++++----------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c5a5fe755..4a38e1a66 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -68,15 +68,15 @@ نظرة على الوسائط والمعلومات في AOD وضع التوصيل حافظ على التراكب المرئي دائماً أثناء تشغيل الموسيقى على AOD - Respect new notifications - Briefly disable for new notifications + احترام الاشعارات الجديدة + تعطيل مؤقت للإشعارات الجديدة أشكال عشوائية استخدم أشكال مختلفة من Material 3 لغلاف الألبوم Album art Clock - Always use while charging - Force Fill mode when the device is plugged in regardless of the selected mode - Size + استعمال دائمًا أثناء الشحن + وضع التعبئة الإجبارية عند توصيل الجهاز بغض النظر عن الوضع المحدد + الحجم لمحة الإشعارات احتفظ بـ AOD قيد التشغيل أثناء انتظار الإشعارات نفس التطبيقات التي تعمل مع إضاءة الإشعارات @@ -1307,8 +1307,10 @@ تطبيقات الحجم والرسوم المتحركة ضبط حجم النظام والرسوم المتحركة - Screen Refresh Rate - Set a fixed or ranged screen refresh rate + معدل تحديث الشاشة + تعيين معدل تحديث ثابت أو متغير للشاشة + +معدل التحديث نص مقياس الخط وزن الخط @@ -1325,14 +1327,14 @@ Set a custom screen refresh rate using Shizuku. Fixed mode locks both the minimum and peak refresh rate to one value, while range mode lets you define separate minimum and peak values. Reset clears the custom override and returns the device to its system-managed refresh behavior. Mode Refresh Rate - Fixed - Range - Fixed refresh rate - Set both minimum and peak refresh rate to the same value - Minimum refresh rate - Choose the lowest refresh rate the display may use - Peak refresh rate - Choose the highest refresh rate the display may use + ثابت + مدى + معدل تحديث ثابت + حدد كل من معدل التحديث الأدنى والذروة إلى نفس القيمة + الحد الأدنى لمعدل التحديث + اختر أدنى معدل تحديث قد تستخدمه الشاشة + ذروة معدل التحديث + اختر أعلى معدل تحديث قد تستخدمه الشاشة. System Remove the custom override and return to the system default refresh behavior Shizuku permission required to adjust refresh rate @@ -1401,8 +1403,8 @@ اخرس انا اعرف ماذا افعل Shut-Up! bypasses app restrictions and checks for developer options, accessibility and debugging by dynamically hiding them when you launch sensitive apps. \n\nUse the custom shortcuts to ensure settings are hidden before the app launches. - Select Apps - Choose apps to apply Shut-Up! logic + حدد التطبيقات + اختر التطبيقات لتطبيق منطق اخرس! Shut-Up! Settings Shut-Up! Features Disable Developer Options @@ -1420,7 +1422,7 @@ Abort ساعة شاشة القفل - تخصيص ساعة شاشة القفل على أجهزة بيكسل + تخصيص ساعة شاشة القفل على أجهزة بكسل Change the lock screen clock style on Pixels. Select from the available clock faces provided by the System UI. Some old styles might not be available. Big Number Calligraphy @@ -1437,10 +1439,10 @@ Clock style applied WRITE_SECURE_SETTINGS permission required Default - Weight - Width + الثقل + العرض Grade - Roundness + الاستدارة Lightness Variation Color @@ -1578,13 +1580,22 @@ المتَرجم مزامنة المكالمات والتحكم بها من ساعتك مزامنة المكالمات - مزامنة الاشعارات - الى مشغل Essentials - استبدال شريط البحث - استبدال مشغل Pixel الافتراضيsearchbar - وقت الغفوة الافتراضي - الخيار الاول + مزامنة الأشعار + لي Essentials launcher + استبدال Searchbar + استبدل شريط البحث الافتراضي Pixel Launcher. + وقت غفوة الافتراضي + الخيار الأول الخيار الثاني الخيار الثالث الخيار الرابع + فارغ + التاريخ + حشوة أفقية + حشوة عمودية + الخلفية + اختر التطبيقات + سيتم مزامنة التطبيقات المحددة فقط للساعه + تأخر الاستعادة + وقت الانتظار قبل استعادة الإعدادات بعد الخروج من تطبيق أخرس! From 7bdbf2cce343cd44d4608bf43aaad5b25b790466 Mon Sep 17 00:00:00 2001 From: fishqaq123 <145851012+fishqaq123@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:47:46 +0000 Subject: [PATCH 4/9] Community translations by @fishqaq123 --- app/src/main/res/values-zh/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d0d692436..02fd4542c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1538,4 +1538,6 @@ 显示无线显示认证选项 移动数据始终开启 即使已连接 Wi-Fi,也要保持移动数据开启状态 + 网络 + 控制频率和网络 From a81e8e2e86cd84557fc19a94d546b8e4e6193a12 Mon Sep 17 00:00:00 2001 From: dimzhen <23508487+dimzhen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:07:30 +0000 Subject: [PATCH 5/9] Community translations by @dimzhen --- app/src/main/res/values-in-rID/strings.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 2fcbc3a9e..37cd25237 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -182,10 +182,10 @@ Lewati notifikasi bisu Skip persistent notifications Flashlight Pulse - Flashlight pulse - Only while facing down + Lampu kilat berdetak + Hanya saat menghadapi ke bawah Same apps as notification lighting - Max brightness + Kecerahan Maks Do not trigger on DND Preview pulse Style @@ -207,7 +207,7 @@ Kiri Tengah Kanan - Select Shapes + Pilih Bentuk Choose which shapes are used in the sweep animation Animasi Pulse count @@ -426,7 +426,7 @@ Untuk semua perangkat Android Cahaya pemberitahuan Nyalakan untuk pemberitahuan - Pulse the flashlight for notifications + Detakkan lampu kilat untuk pemberitahuan Sound mode tile Call vibrations Vibrate for call actions @@ -1296,7 +1296,7 @@ README Refresh - Sound mode tile + Mode ubin suara QS tile to toggle sound mode Show slider Show volume slider in tile @@ -1684,10 +1684,10 @@ a few minutes Translations - Translation Mode - Long press any text to translate and contribute to the community - Translated - %d edit(s) pending — tap to submit or discard + Mode Terjemahan + Tekan lama tulisan apapun untuk menerjemahkan dan kontribusi ke komunitas + Diterjemahkan + %d perubahan dijeda — ketuk untuk mengirim atau membatalkan Submit Discard Translate From b2c8794b6177fb4e6ab6fd02cb8f3cbaf01fe325 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 19 Aug 2026 11:18:38 +0530 Subject: [PATCH 6/9] feat: custm settings in DIY --- .../sameerasw/essentials/domain/diy/Action.kt | 41 ++ .../domain/registry/PermissionRegistry.kt | 7 +- .../executors/CombinedActionExecutor.kt | 25 + .../ui/activities/AutomationEditorActivity.kt | 26 + .../ui/core/sheets/CustomSettingsSheet.kt | 583 ++++++++++++++++++ .../essentials/utils/SettingsRecorder.kt | 94 +++ app/src/main/res/values/strings.xml | 17 + 7 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CustomSettingsSheet.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/SettingsRecorder.kt 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 3efa21ed4..1c54598ae 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,46 @@ sealed interface Action { override val permissions: List = listOf("SHIZUKU", "ROOT") override val isConfigurable: Boolean = true } + + @Keep + enum class SettingsTable { + @SerializedName("SYSTEM") + SYSTEM, + + @SerializedName("SECURE") + SECURE, + + @SerializedName("GLOBAL") + GLOBAL + } + + @Keep + data class SettingsEntry( + @SerializedName("table") val table: SettingsTable = SettingsTable.SYSTEM, + @SerializedName("key") val key: String = "", + @SerializedName("value") val value: String = "" + ) + + @Keep + data class CustomSettings( + @SerializedName("entries") val entries: List = emptyList() + ) : Action { + override val title: Int get() = R.string.diy_action_custom_settings + override val icon: Int get() = R.drawable.rounded_settings_24 + override val isConfigurable: Boolean = true + override val permissions: List + get() { + val perms = mutableSetOf() + for (entry in entries) { + when (entry.table) { + SettingsTable.SYSTEM -> perms.add("WRITE_SETTINGS") + SettingsTable.SECURE -> perms.add("WRITE_SECURE_SETTINGS") + SettingsTable.GLOBAL -> perms.add("WRITE_SECURE_SETTINGS") + } + } + return perms.toList() + } + } } + diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt index f063f7f42..e53287207 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt @@ -135,9 +135,6 @@ fun initPermissionRegistry() { // Safe Volume Warning feature PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_safe_volume_title) - // Notification Snoozing feature - PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.feat_notification_snoozing_title) - // Install unknown packages feature // Watch Call Sync & Notification Sync PermissionRegistry.register("READ_PHONE_STATE", R.string.watch_call_sync_title) @@ -145,4 +142,8 @@ fun initPermissionRegistry() { PermissionRegistry.register("READ_CONTACTS", R.string.watch_call_sync_title) PermissionRegistry.register("READ_CALL_LOG", R.string.watch_call_sync_title) PermissionRegistry.register("NOTIFICATION_LISTENER", R.string.watch_notif_sync_title) + + // DIY Automations feature + PermissionRegistry.register("WRITE_SECURE_SETTINGS", R.string.tab_diy) + PermissionRegistry.register("WRITE_SETTINGS", R.string.tab_diy) } 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 43ea619f6..fc937c867 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,14 @@ import android.hardware.camera2.CameraManager import android.media.AudioManager import android.net.wifi.WifiManager import android.os.Build +import android.provider.Settings 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.ShellUtils import com.sameerasw.essentials.utils.performHapticFeedback import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper @@ -650,6 +652,29 @@ object CombinedActionExecutor { com.sameerasw.essentials.utils.FreezeManager.unfreezeApp(context, pkg) } } + is Action.CustomSettings -> { + val resolver = context.contentResolver + for (entry in action.entries) { + val success = try { + when (entry.table) { + Action.SettingsTable.SYSTEM -> Settings.System.putString(resolver, entry.key, entry.value) + Action.SettingsTable.SECURE -> Settings.Secure.putString(resolver, entry.key, entry.value) + Action.SettingsTable.GLOBAL -> Settings.Global.putString(resolver, entry.key, entry.value) + } + } catch (e: Exception) { + false + } + if (!success) { + val tableArg = when (entry.table) { + Action.SettingsTable.SYSTEM -> "system" + Action.SettingsTable.SECURE -> "secure" + Action.SettingsTable.GLOBAL -> "global" + } + val safeValue = if (entry.value.contains(" ")) "\"${entry.value}\"" else entry.value + ShellUtils.runCommand(context, "settings put $tableArg ${entry.key} $safeValue") + } + } + } } } 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 4469dd4f8..50ec80554 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 @@ -96,6 +96,7 @@ import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker import com.sameerasw.essentials.ui.core.sheets.AppSelectionSheet import com.sameerasw.essentials.ui.core.sheets.BluetoothDeviceSelectionSheet +import com.sameerasw.essentials.ui.core.sheets.CustomSettingsSheet import com.sameerasw.essentials.ui.core.sheets.DimWallpaperSettingsSheet import com.sameerasw.essentials.ui.core.sheets.ScreenOffSettingsSheet import com.sameerasw.essentials.ui.core.sheets.SingleAppSelectionSheet @@ -265,6 +266,7 @@ class AutomationEditorActivity : ComponentActivity() { var showTimeSettings by remember { mutableStateOf(false) } var showBluetoothSettings by remember { mutableStateOf(false) } var showWifiSettings by remember { mutableStateOf(false) } + var showCustomSettingsSettings by remember { mutableStateOf(false) } var configAction by remember { mutableStateOf(null) } // Generic config action val isTriggerConfigured = when (val trigger = selectedTrigger) { @@ -295,6 +297,7 @@ class AutomationEditorActivity : ComponentActivity() { fun isActionConfigured(action: Action?): Boolean = when (action) { is Action.OpenApp -> action.packageName.isNotBlank() + is Action.CustomSettings -> action.entries.isNotEmpty() else -> true } @@ -910,6 +913,7 @@ class AutomationEditorActivity : ComponentActivity() { Action.ToggleFlashlight, Action.TurnOnLowPower, Action.TurnOffLowPower, + Action.CustomSettings(), Action.CircleToSearch, Action.TakeScreenshot, Action.ShowNotification, @@ -996,6 +1000,7 @@ class AutomationEditorActivity : ComponentActivity() { temporarySelectedAppsForAction = resolvedAction.packageNames showFreezeAppsSettings = true } + is Action.CustomSettings -> showCustomSettingsSettings = true else -> {} } } @@ -1279,6 +1284,27 @@ class AutomationEditorActivity : ComponentActivity() { excludePackages = if (automationType == Automation.Type.APP) selectedApps else emptyList() ) } + + if (showCustomSettingsSettings && configAction is Action.CustomSettings) { + CustomSettingsSheet( + initialAction = configAction as Action.CustomSettings, + onDismiss = { showCustomSettingsSettings = false }, + onSave = { newAction -> + showCustomSettingsSettings = false + when (automationType) { + Automation.Type.TRIGGER -> selectedAction = newAction + Automation.Type.ACTION_SHORTCUT, Automation.Type.PIXEL_SEARCHBAR -> selectedAction = + newAction + + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = newAction + else selectedOutAction = newAction + } + } + configAction = null + } + ) + } } if (showPermissionSheet) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CustomSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CustomSettingsSheet.kt new file mode 100644 index 000000000..65af6880a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CustomSettingsSheet.kt @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: UI Feature - System + * File: CustomSettingsSheet.kt + * Description: Bottom sheet allowing recording, editing, and saving of custom Android settings entries. + */ + +package com.sameerasw.essentials.ui.core.sheets + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonGroupDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.ColorUtil +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.SettingsRecorder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private enum class RecordingState { + IDLE, + RECORDING +} + +private const val MAX_ENTRIES_LIMIT = 20 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomSettingsSheet( + initialAction: Action.CustomSettings, + onDismiss: () -> Unit, + onSave: (Action.CustomSettings) -> Unit +) { + val context = LocalContext.current + val view = LocalView.current + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scrollState = rememberScrollState() + + var recordingState by remember { mutableStateOf(RecordingState.IDLE) } + var beforeSnapshot by remember { mutableStateOf>(emptyMap()) } + var elapsedSeconds by remember { mutableIntStateOf(0) } + var showHowItWorksExpanded by remember { mutableStateOf(false) } + + val entries = remember { + mutableStateListOf().apply { + addAll(initialAction.entries) + } + } + + LaunchedEffect(recordingState) { + if (recordingState == RecordingState.RECORDING) { + elapsedSeconds = 0 + while (true) { + delay(1000L) + elapsedSeconds++ + } + } + } + + EssentialsBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .imePadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Title Header + Text( + text = stringResource(R.string.diy_action_custom_settings), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .clip(RoundedCornerShape(24.dp)) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .clickable { + HapticUtil.performUIHaptic(view) + showHowItWorksExpanded = !showHowItWorksExpanded + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + val infoTitle = stringResource(R.string.diy_custom_settings_how_it_works) + Box( + modifier = Modifier + .size(40.dp) + .background( + color = ColorUtil.getPastelColorFor(infoTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.rounded_info_24), + contentDescription = null, + tint = ColorUtil.getVibrantColorFor(infoTitle), + modifier = Modifier.size(24.dp) + ) + } + Text( + text = stringResource(R.string.diy_custom_settings_how_it_works), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource( + if (showHowItWorksExpanded) R.drawable.rounded_keyboard_arrow_up_24 + else R.drawable.rounded_keyboard_arrow_down_24 + ), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } + + AnimatedVisibility( + visible = showHowItWorksExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.diy_custom_settings_how_to), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(12.dp), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + painter = painterResource(R.drawable.rounded_release_alert_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = stringResource(R.string.diy_custom_settings_warning_title), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.error + ) + Text( + text = stringResource(R.string.diy_custom_settings_warning_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + } + + if (recordingState == RecordingState.IDLE) { + Button( + onClick = { + HapticUtil.performHeavyHaptic(view) + scope.launch { + val snapshot = withContext(Dispatchers.IO) { + SettingsRecorder.snapshot(context) + } + beforeSnapshot = snapshot + recordingState = RecordingState.RECORDING + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + painter = painterResource(R.drawable.rounded_play_arrow_24), + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(R.string.diy_custom_settings_start_recording), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) + ) + } + } else { + val infiniteTransition = rememberInfiniteTransition(label = "pulse") + val pulseAlpha by infiniteTransition.animateFloat( + initialValue = 0.6f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(600), + repeatMode = RepeatMode.Reverse + ), + label = "pulseAlpha" + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { + HapticUtil.performHeavyHaptic(view) + scope.launch { + val afterSnapshot = withContext(Dispatchers.IO) { + SettingsRecorder.snapshot(context) + } + val newChanges = withContext(Dispatchers.IO) { + SettingsRecorder.diff(beforeSnapshot, afterSnapshot) + } + + for (newEntry in newChanges) { + if (entries.size >= MAX_ENTRIES_LIMIT) { + val existingIndex = entries.indexOfFirst { + it.table == newEntry.table && it.key == newEntry.key + } + if (existingIndex != -1) { + entries[existingIndex] = newEntry + } + continue + } + + val existingIndex = entries.indexOfFirst { + it.table == newEntry.table && it.key == newEntry.key + } + if (existingIndex != -1) { + entries[existingIndex] = newEntry + } else { + entries.add(newEntry) + } + } + recordingState = RecordingState.IDLE + } + }, + modifier = Modifier + .weight(1f) + .height(56.dp), + shape = ButtonGroupDefaults.connectedLeadingButtonShapes().shape, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + ) { + Icon( + painter = painterResource(R.drawable.rounded_stop_circle_24), + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.diy_custom_settings_stop_recording), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + maxLines = 1 + ) + } + + Button( + onClick = {}, + modifier = Modifier + .width(96.dp) + .height(56.dp) + .alpha(pulseAlpha), + shape = ButtonGroupDefaults.connectedTrailingButtonShapes().shape, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(MaterialTheme.colorScheme.error, CircleShape) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "${elapsedSeconds}s", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onErrorContainer, + maxLines = 1 + ) + } + } + } + } + + if (entries.isEmpty()) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.diy_custom_settings_no_entries), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp) + ) + } + } else { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "${stringResource(R.string.diy_action_custom_settings)} (${entries.size}/$MAX_ENTRIES_LIMIT)", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + RoundedCardContainer( + spacing = 2.dp, + cornerRadius = 24.dp + ) { + val count = entries.size + entries.forEachIndexed { index, entry -> + CustomSettingEntryRow( + entry = entry, + index = index, + count = count, + onValueChange = { newValue -> + entries[index] = entry.copy(value = newValue) + }, + onDelete = { + HapticUtil.performLightHaptic(view) + entries.removeAt(index) + } + ) + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 0.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onDismiss() + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_close_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_cancel)) + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onSave(Action.CustomSettings(entries = entries.toList())) + }, + modifier = Modifier.weight(1f), + enabled = entries.isNotEmpty() + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_check_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_save)) + } + } + } + } +} + +@Composable +private fun CustomSettingEntryRow( + entry: Action.SettingsEntry, + index: Int, + count: Int, + onValueChange: (String) -> Unit, + onDelete: () -> Unit +) { + val tableTitle = when (entry.table) { + Action.SettingsTable.SYSTEM -> stringResource(R.string.diy_custom_settings_table_system) + Action.SettingsTable.SECURE -> stringResource(R.string.diy_custom_settings_table_secure) + Action.SettingsTable.GLOBAL -> stringResource(R.string.diy_custom_settings_table_global) + } + + val chipContainerColor = when (entry.table) { + Action.SettingsTable.SYSTEM -> MaterialTheme.colorScheme.primaryContainer + Action.SettingsTable.SECURE -> MaterialTheme.colorScheme.secondaryContainer + Action.SettingsTable.GLOBAL -> MaterialTheme.colorScheme.tertiaryContainer + } + + val chipContentColor = when (entry.table) { + Action.SettingsTable.SYSTEM -> MaterialTheme.colorScheme.onPrimaryContainer + Action.SettingsTable.SECURE -> MaterialTheme.colorScheme.onSecondaryContainer + Action.SettingsTable.GLOBAL -> MaterialTheme.colorScheme.onTertiaryContainer + } + + val shape = when { + count == 1 -> RoundedCornerShape(24.dp) + index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 4.dp, bottomEnd = 4.dp) + index == count - 1 -> RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 24.dp, bottomEnd = 24.dp) + else -> RoundedCornerShape(4.dp) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + SuggestionChip( + onClick = {}, + label = { Text(tableTitle, style = MaterialTheme.typography.labelSmall) }, + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = chipContainerColor, + labelColor = chipContentColor + ), + border = null, + modifier = Modifier.height(26.dp) + ) + + IconButton( + onClick = onDelete, + modifier = Modifier.size(32.dp) + ) { + Icon( + painter = painterResource(R.drawable.rounded_delete_24), + contentDescription = stringResource(R.string.action_delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + } + } + + Text( + text = entry.key, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + + OutlinedTextField( + value = entry.value, + onValueChange = onValueChange, + label = { Text(stringResource(R.string.diy_custom_settings_value_label)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/SettingsRecorder.kt b/app/src/main/java/com/sameerasw/essentials/utils/SettingsRecorder.kt new file mode 100644 index 000000000..02a0f5d37 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/SettingsRecorder.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: Utilities + * File: SettingsRecorder.kt + * Description: Utility to snapshot and diff Android Settings tables (System, Secure, Global). + */ + +package com.sameerasw.essentials.utils + +import android.content.Context +import android.net.Uri +import android.provider.Settings +import com.sameerasw.essentials.domain.diy.Action + +object SettingsRecorder { + + // Sensitive keys that might cause unexpected system disruptions + val SENSITIVE_KEYS: Set = setOf( + "adb_enabled", + "adb_wifi_enabled", + "development_settings_enabled", + "device_provisioned", + "user_setup_complete", + ) + + fun isSensitiveKey(key: String): Boolean { + return SENSITIVE_KEYS.any { it.equals(key, ignoreCase = true) } + } + + fun snapshot(context: Context): Map { + val result = mutableMapOf() + val resolver = context.contentResolver + + fun readTable(uri: Uri, table: Action.SettingsTable) { + try { + resolver.query(uri, arrayOf("name", "value"), null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex("name") + val valueIndex = cursor.getColumnIndex("value") + if (nameIndex != -1 && valueIndex != -1) { + while (cursor.moveToNext()) { + val name = cursor.getString(nameIndex) + val value = cursor.getString(valueIndex) ?: "" + if (name != null) { + result["${table.name}/$name"] = value + } + } + } + } + } catch (e: Exception) { + // Ignore errors reading protected tables if permissions are restricted + } + } + + readTable(Settings.System.CONTENT_URI, Action.SettingsTable.SYSTEM) + readTable(Settings.Secure.CONTENT_URI, Action.SettingsTable.SECURE) + readTable(Settings.Global.CONTENT_URI, Action.SettingsTable.GLOBAL) + + return result + } + + fun diff( + before: Map, + after: Map + ): List { + val changes = mutableListOf() + + for ((keyWithTable, afterValue) in after) { + val beforeValue = before[keyWithTable] + if (beforeValue != afterValue) { + val parts = keyWithTable.split("/", limit = 2) + if (parts.size == 2) { + val tableName = parts[0] + val key = parts[1] + val table = try { + Action.SettingsTable.valueOf(tableName) + } catch (e: Exception) { + Action.SettingsTable.SYSTEM + } + changes.add( + Action.SettingsEntry( + table = table, + key = key, + value = afterValue + ) + ) + } + } + } + + return changes + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a4316f02..f2bf81a53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1036,6 +1036,23 @@ Disabled Sound Mode This action allows switching between Sound, Vibrate, and Silent modes based on triggers. It requires Do Not Disturb access. + Custom Settings + Capture Android system settings changes and replay them automatically as an action. + How it works + Tap Start Recording, adjust any system setting. And then return and tap Stop Recording. Review and edit the detected changes below. + Start Recording + Stop Recording + Recording active… + Nothing captured yet + No settings changes detected during the recording session. + Maximum limit of 20 settings reached. + Warning + Some captured keys might affect system behavior or require Write Secure Settings permissions. Modify with caution. + System + Secure + Global + Setting Key + Value Sameera Wijerathna From b9d00c7c1bf1560a332d6cd43856b9fd612168de Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 19 Aug 2026 11:21:23 +0530 Subject: [PATCH 7/9] revert: remove temp easter egg --- .../ui/composables/SetupFeatures.kt | 83 ++++++------------- .../res/drawable/essentials_instagram.xml | 42 ---------- 2 files changed, 27 insertions(+), 98 deletions(-) delete mode 100644 app/src/main/res/drawable/essentials_instagram.xml diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt index 1ee4bf5dc..bfa4f0fd7 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt @@ -16,16 +16,12 @@ import android.content.Context import android.content.Intent import android.provider.Settings import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -1047,14 +1043,6 @@ fun SetupFeatures( val isSearchingViewModel = viewModel.isSearching.value val recentSearches by viewModel.recentSearches - var showInstagramGraphic by rememberSaveable { mutableStateOf(true) } - LaunchedEffect(Unit) { - if (showInstagramGraphic) { - delay(5000) - showInstagramGraphic = false - } - } - LazyColumn( state = lazyListState, modifier = modifier.fillMaxSize(), @@ -1090,55 +1078,38 @@ fun SetupFeatures( verticalArrangement = Arrangement.Center ) { Spacer(modifier = Modifier.height(statusBarPadding)) - AnimatedContent( - targetState = showInstagramGraphic, - transitionSpec = { - fadeIn(animationSpec = tween(600)) togetherWith fadeOut(animationSpec = tween(600)) - }, - label = "InstagramTitleSwap" - ) { isShowingInstagram -> - if (isShowingInstagram) { - Icon( - painter = painterResource(id = R.drawable.essentials_instagram), - contentDescription = null, - modifier = Modifier.height(28.dp), - tint = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface - ) - } else { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .width(chevronWidth) + .graphicsLayer { alpha = chevronAlpha } + ) { Row(verticalAlignment = Alignment.CenterVertically) { - Box( + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null, modifier = Modifier - .width(chevronWidth) - .graphicsLayer { alpha = chevronAlpha } - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier - .size(24.dp) - .graphicsLayer { - rotationZ = - (displayFraction * 180f).coerceIn(0f, 180f) - }, - tint = contentColor - ) - Spacer(modifier = Modifier.width(8.dp)) - } - } - Text( - text = deviceInfo.deviceName, - style = MaterialTheme.typography.titleMedium.copy( - fontFamily = fontFamily - ), - modifier = Modifier.graphicsLayer { - scaleX = textScale - scaleY = textScale - }, - color = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + .size(24.dp) + .graphicsLayer { + rotationZ = + (displayFraction * 180f).coerceIn(0f, 180f) + }, + tint = contentColor ) + Spacer(modifier = Modifier.width(8.dp)) } } + Text( + text = deviceInfo.deviceName, + style = MaterialTheme.typography.titleMedium.copy( + fontFamily = fontFamily + ), + modifier = Modifier.graphicsLayer { + scaleX = textScale + scaleY = textScale + }, + color = if (thresholdPassed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ) } } } diff --git a/app/src/main/res/drawable/essentials_instagram.xml b/app/src/main/res/drawable/essentials_instagram.xml deleted file mode 100644 index ad871a144..000000000 --- a/app/src/main/res/drawable/essentials_instagram.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - From c18be7074ab4872a4d00db298564c16bfc99110f Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 19 Aug 2026 13:46:28 +0530 Subject: [PATCH 8/9] feat: add default icons for system status items and improve resource validation in FeatureCard --- .../essentials/domain/StatusBarIcon.kt | 8 ++++---- .../essentials/ui/core/cards/FeatureCard.kt | 20 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt index 72fec4c48..1603264c5 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/StatusBarIcon.kt @@ -283,7 +283,7 @@ object StatusBarIconRegistry { displayNameRes = R.string.icon_network_speed, blacklistNames = listOf("network_speed"), categoryRes = CAT_SYSTEM_STATUS, - iconRes = null + iconRes = R.drawable.rounded_shutter_speed_24 ), // --- OEM Specific --- @@ -312,21 +312,21 @@ object StatusBarIconRegistry { displayNameRes = R.string.icon_glove_mode, blacklistNames = listOf("glove_mode", "glove"), categoryRes = CAT_OEM_SPECIFIC, - iconRes = null + iconRes = R.drawable.rounded_touch_app_24 ), StatusBarIcon( id = "frame_rate", displayNameRes = R.string.icon_frame_rate, blacklistNames = listOf("frame_rate_mode"), categoryRes = CAT_OEM_SPECIFIC, - iconRes = null + iconRes = R.drawable.rounded_shutter_speed_24 ), StatusBarIcon( id = "no_sim", displayNameRes = R.string.icon_no_sim, blacklistNames = listOf("nosim"), categoryRes = CAT_PHONE_NETWORK, - iconRes = null + iconRes = R.drawable.rounded_signal_cellular_alt_24 ), StatusBarIcon( id = "samsung_services", diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt index bfcf5cab2..a4bfb21ee 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/FeatureCard.kt @@ -152,23 +152,19 @@ fun FeatureCard( ) } else if (iconRes != null) { val context = LocalContext.current - val isValid = remember(iconRes) { + val validIconRes = remember(iconRes) { try { - val value = android.util.TypedValue() - context.resources.getValue(iconRes, value, true) - val path = value.string?.toString() ?: "" - !path.endsWith(".gif", ignoreCase = true) + if (iconRes != 0 && context.resources.getResourceTypeName(iconRes) == "drawable") { + iconRes + } else { + R.drawable.rounded_settings_24 + } } catch (e: Throwable) { - false + R.drawable.rounded_settings_24 } } - val painter = if (isValid) { - painterResource(id = iconRes) - } else { - painterResource(id = R.drawable.rounded_settings_accessibility_24) - } Icon( - painter = painter, + painter = painterResource(id = validIconRes), contentDescription = resolvedTitle, modifier = Modifier.size(24.dp), tint = ColorUtil.getVibrantColorFor(resolvedTitle) From eb862074f69fac74da4c2f7879cba894fc0f495b Mon Sep 17 00:00:00 2001 From: Ndraaa-repo <273137447+Ndraaa-repo@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:16:28 +0000 Subject: [PATCH 9/9] Community translations by @Ndraaa-repo --- app/src/main/res/values-id/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 89021c167..7b2ddf5bb 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -43,7 +43,7 @@ Anda dapat menyesuaikan gaya animasi, warna, dan perilakunya. Fitur ini berfungs Otomatis Geser antar tab Di layar utama - Umpan Balik Haptik + Umpan balik haptik Aktifkan efek blur progresif pada elemen di seluruh antarmuka pengguna. Alih-alih fitur aksesibilitas (Bekukan, Kunci Aplikasi, Lampu Malam Dinamis) Aktifkan fitur yang tidak didukung @@ -51,4 +51,6 @@ Anda dapat menyesuaikan gaya animasi, warna, dan perilakunya. Fitur ini berfungs Mode translasi Tekan lama teks mana pun untuk menerjemahkan dan berkontribusi pada komunitas. Aksesibilitas + Aksesibilitas otomatis + Secara otomatis memberikan izin aksesibilitas saat aplikasi diluncurkan jika izin tersebut hilang menggunakan WRITE_SECURE_SETTINGS. \ No newline at end of file