From e2bb4ab661a8b937e10db4dafeada44b118eb985 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 21:21:47 +0530 Subject: [PATCH 01/17] refactor: manual crash capture and share --- .../com/sameerasw/essentials/EssentialsApp.kt | 16 ++- .../ui/activities/SettingsActivity.kt | 81 +++++++++++ .../sameerasw/essentials/utils/LogManager.kt | 134 ++++++++++++++---- app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/file_paths.xml | 8 +- 5 files changed, 217 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt index 32b954ca5..f344614a9 100644 --- a/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt +++ b/app/src/main/java/com/sameerasw/essentials/EssentialsApp.kt @@ -96,7 +96,21 @@ class EssentialsApp : Application() { "https://e105699467efe3a43a16bfbad3a63b33@o4510996760887296.ingest.de.sentry.io/4510996763312208" options.isEnabled = true - options.setBeforeSend { event, _ -> + options.setBeforeSend { event, hint -> + try { + val throwable = event.throwable + val threadName = event.threads?.firstOrNull()?.name ?: Thread.currentThread().name + val message = event.message?.formatted ?: event.exceptions?.firstOrNull()?.value + com.sameerasw.essentials.utils.LogManager.saveCrashReport( + context = this@EssentialsApp, + threadName = threadName, + throwable = throwable, + customMessage = message, + ) + } catch (e: Exception) { + android.util.Log.e("EssentialsApp", "Failed to save crash report locally in Sentry callback", e) + } + Handler(Looper.getMainLooper()).post { Toast .makeText( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index b6db2802b..86a12ef9e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -124,7 +124,10 @@ import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.utils.PermissionUIHelper import com.sameerasw.essentials.viewmodels.GitHubAuthViewModel import com.sameerasw.essentials.viewmodels.MainViewModel +import androidx.core.content.FileProvider +import com.sameerasw.essentials.utils.LogManager import rikka.shizuku.Shizuku +import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -1264,6 +1267,84 @@ fun SettingsContent( } } + Row( + modifier = + Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall, + ).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + val latestCrash = LogManager.getLatestCrashReport(context) + if (latestCrash != null && latestCrash.exists()) { + try { + val uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + latestCrash + ) + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_SUBJECT, "Essentials Crash Log - ${latestCrash.name}") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(shareIntent, null)) + } catch (e: Exception) { + Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() + } + } else { + Toast.makeText(context, context.getString(R.string.toast_no_crash_logs), Toast.LENGTH_SHORT).show() + } + }, + modifier = + Modifier + .weight(1f) + .height(40.dp), + shape = ButtonDefaults.shape, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = stringResource(R.string.btn_share_crash_logs), + style = MaterialTheme.typography.labelMedium, + ) + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + LogManager.clearAllCrashReports(context) + Toast.makeText(context, context.getString(R.string.toast_crash_logs_cleared), Toast.LENGTH_SHORT).show() + }, + modifier = + Modifier + .weight(1f) + .height(40.dp), + shape = ButtonDefaults.shape, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = stringResource(R.string.btn_clear_crash_logs), + style = MaterialTheme.typography.labelMedium, + ) + } + } + Row( modifier = Modifier diff --git a/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt index 5b611a7fe..29fdf6ae3 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt @@ -27,6 +27,8 @@ object LogManager { private const val MAX_LOG_SIZE = 500 private val logBuffer = LinkedList() private const val CRASH_LOG_FILENAME = "last_crash.log" + private const val CRASH_REPORTS_DIR = "crash_reports" + private const val MAX_CRASH_REPORTS = 10 private var lastCrashLog: String? = null private val isInitialized = AtomicBoolean(false) @@ -46,9 +48,6 @@ object LogManager { if (crashFile.exists()) { try { lastCrashLog = crashFile.readText() - // delete after reading so we don't report old crashes forever? - // meaningful to keep it until a successful report? Let's keep it for now but maybe we can clear it if needed. - // For now, let's keep it. } catch (e: Exception) { Log.e("LogManager", "Failed to read crash log", e) } @@ -67,41 +66,126 @@ object LogManager { } } - private fun handleCrash( + fun getCrashReportsDirectory(context: Context): File { + val externalDir = context.getExternalFilesDir(CRASH_REPORTS_DIR) + val dir = if (externalDir != null) { + externalDir + } else { + File(context.filesDir, CRASH_REPORTS_DIR) + } + if (!dir.exists()) { + dir.mkdirs() + } + return dir + } + + fun getAllCrashReports(context: Context): List { + val dir = getCrashReportsDirectory(context) + return dir.listFiles { file -> file.isFile && (file.extension == "log" || file.extension == "txt") } + ?.sortedByDescending { it.lastModified() } + ?: emptyList() + } + + fun getLatestCrashReport(context: Context): File? { + return getAllCrashReports(context).firstOrNull() + } + + fun clearAllCrashReports(context: Context) { + try { + getCrashReportsDirectory(context).listFiles()?.forEach { it.delete() } + File(context.filesDir, CRASH_LOG_FILENAME).delete() + lastCrashLog = null + } catch (e: Exception) { + Log.e("LogManager", "Failed to clear crash reports", e) + } + } + + private fun pruneOldCrashReports(context: Context) { + try { + val reports = getAllCrashReports(context) + if (reports.size > MAX_CRASH_REPORTS) { + reports.drop(MAX_CRASH_REPORTS).forEach { it.delete() } + } + } catch (e: Exception) { + Log.e("LogManager", "Failed to prune crash reports", e) + } + } + + fun saveCrashReport( context: Context, - thread: Thread, - throwable: Throwable, - ) { - val sw = StringWriter() - val pw = PrintWriter(sw) - throwable.printStackTrace(pw) - val stackTrace = sw.toString() - - val report = - buildString { - append("Crash Time: ${formatDate(System.currentTimeMillis())}\n") - append("Thread: ${thread.name}\n") - append("Exception: ${throwable.javaClass.simpleName}\n") + threadName: String = Thread.currentThread().name, + throwable: Throwable? = null, + customMessage: String? = null, + ): File? { + val timestamp = System.currentTimeMillis() + val fileDateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date(timestamp)) + val stackTrace = throwable?.let { + val sw = StringWriter() + it.printStackTrace(PrintWriter(sw)) + sw.toString() + } ?: "" + + val appVersion = try { + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + "${pInfo.versionName} (${if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode else @Suppress("DEPRECATION") pInfo.versionCode})" + } catch (e: Exception) { + "Unknown" + } + + val report = buildString { + append("Crash Time: ${formatDate(timestamp)}\n") + append("App Version: $appVersion\n") + append("Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})\n") + append("Android OS: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\n") + append("Thread: $threadName\n") + if (throwable != null) { + append("Exception: ${throwable.javaClass.name}\n") append("Message: ${throwable.message}\n") append("Stack Trace:\n$stackTrace\n") - append("\n--- Last Logs before crash ---\n") - synchronized(logBuffer) { - // Take last 50 logs for context - logBuffer.takeLast(50).forEach { entry -> - append(formatLogEntry(entry)) - append("\n") - } + } + if (!customMessage.isNullOrBlank()) { + append("Details:\n$customMessage\n") + } + append("\n--- Last Logs before crash ---\n") + synchronized(logBuffer) { + logBuffer.takeLast(50).forEach { entry -> + append(formatLogEntry(entry)) + append("\n") } } + } - try { + return try { + // Write legacy last_crash.log val crashFile = File(context.filesDir, CRASH_LOG_FILENAME) crashFile.writeText(report) + lastCrashLog = report + + // Write dated file in crash_reports directory + val reportsDir = getCrashReportsDirectory(context) + val logFile = File(reportsDir, "crash_$fileDateFormat.log") + logFile.writeText(report) + + pruneOldCrashReports(context) + logFile } catch (e: Exception) { Log.e("LogManager", "Failed to write crash log", e) + null } } + private fun handleCrash( + context: Context, + thread: Thread, + throwable: Throwable, + ) { + saveCrashReport( + context = context, + threadName = thread.name, + throwable = throwable, + ) + } + fun log( tag: String, message: String, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f7f79162..f9bffe952 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -914,8 +914,12 @@ Crash reporting Off Auto - Essentials crashed, Report sent + Essentials crashed, report saved and sent Simulate crash + Share latest crash log + Clear crash logs + Crash logs cleared + No crash logs found Welcome to Essentials A Toolbox for Android Nerds by sameerasw.com diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 5da371043..0c57bab52 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -9,7 +9,13 @@ + + From bcdd36da9f991fd6a4236afd38f327c9dfc765c9 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 21:44:10 +0530 Subject: [PATCH 02/17] feat: implement CrashLogsBottomSheet --- .../ui/activities/SettingsActivity.kt | 96 ++------ .../ui/core/sheets/CrashLogsBottomSheet.kt | 225 ++++++++++++++++++ .../sameerasw/essentials/utils/LogManager.kt | 2 +- app/src/main/res/values/strings.xml | 5 +- 4 files changed, 246 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CrashLogsBottomSheet.kt diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index 86a12ef9e..7f762225b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -124,8 +124,7 @@ import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.utils.PermissionUIHelper import com.sameerasw.essentials.viewmodels.GitHubAuthViewModel import com.sameerasw.essentials.viewmodels.MainViewModel -import androidx.core.content.FileProvider -import com.sameerasw.essentials.utils.LogManager +import com.sameerasw.essentials.ui.core.sheets.CrashLogsBottomSheet import rikka.shizuku.Shizuku import java.io.File import java.text.SimpleDateFormat @@ -335,6 +334,7 @@ fun SettingsContent( val isRootPermissionGranted by viewModel.isRootPermissionGranted val isDeveloperModeEnabled by viewModel.isDeveloperModeEnabled var showInstructionsSheet by remember { mutableStateOf(false) } + var showCrashLogsSheet by remember { mutableStateOf(false) } var showShizukuHelpBottomSheet by remember { mutableStateOf(false) } var showUnsupportedFeaturesSheet by remember { mutableStateOf(false) } var showPreReleaseConfirmSheet by remember { mutableStateOf(false) } @@ -454,6 +454,12 @@ fun SettingsContent( ) } + if (showCrashLogsSheet) { + CrashLogsBottomSheet( + onDismissRequest = { showCrashLogsSheet = false }, + ) + } + if (showUnsupportedFeaturesSheet) { UnsupportedFeaturesConfirmationSheet( onDismissRequest = { showUnsupportedFeaturesSheet = false }, @@ -1267,83 +1273,15 @@ fun SettingsContent( } } - Row( - modifier = - Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = Shapes.extraSmall, - ).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - val latestCrash = LogManager.getLatestCrashReport(context) - if (latestCrash != null && latestCrash.exists()) { - try { - val uri = FileProvider.getUriForFile( - context, - "${context.packageName}.fileprovider", - latestCrash - ) - val shareIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_SUBJECT, "Essentials Crash Log - ${latestCrash.name}") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - context.startActivity(Intent.createChooser(shareIntent, null)) - } catch (e: Exception) { - Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() - } - } else { - Toast.makeText(context, context.getString(R.string.toast_no_crash_logs), Toast.LENGTH_SHORT).show() - } - }, - modifier = - Modifier - .weight(1f) - .height(40.dp), - shape = ButtonDefaults.shape, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = stringResource(R.string.btn_share_crash_logs), - style = MaterialTheme.typography.labelMedium, - ) - } - - Button( - onClick = { - HapticUtil.performVirtualKeyHaptic(view) - LogManager.clearAllCrashReports(context) - Toast.makeText(context, context.getString(R.string.toast_crash_logs_cleared), Toast.LENGTH_SHORT).show() - }, - modifier = - Modifier - .weight(1f) - .height(40.dp), - shape = ButtonDefaults.shape, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - contentPadding = PaddingValues(0.dp), - ) { - Text( - text = stringResource(R.string.btn_clear_crash_logs), - style = MaterialTheme.typography.labelMedium, - ) - } - } + IconToggleItem( + iconRes = R.drawable.rounded_bug_report_24, + title = stringResource(R.string.crash_logs_title), + description = stringResource(R.string.crash_logs_desc), + showToggle = false, + onClick = { + showCrashLogsSheet = true + }, + ) Row( modifier = diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CrashLogsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CrashLogsBottomSheet.kt new file mode 100644 index 000000000..f60e3bf1f --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/CrashLogsBottomSheet.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2026 sameerasw.com + * License: MIT License + * + * Feature Module: UI Core Sheets + * File: CrashLogsBottomSheet.kt + * Description: Bottom sheet displaying recent crash report logs with sharing and deletion controls. + */ + +package com.sameerasw.essentials.ui.core.sheets + +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.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 androidx.core.content.FileProvider +import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.theme.Shapes +import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.LogManager +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CrashLogsBottomSheet( + onDismissRequest: () -> Unit, +) { + val context = LocalContext.current + val view = LocalView.current + var crashReports by remember { + mutableStateOf(LogManager.getAllCrashReports(context).take(5)) + } + + val displayDateFormat = remember { + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + } + + EssentialsBottomSheet( + onDismissRequest = onDismissRequest, + ) { + Column( + modifier = + Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.crash_logs_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + + if (crashReports.isEmpty()) { + Text( + text = stringResource(R.string.toast_no_crash_logs), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 32.dp), + ) + } else { + RoundedCardContainer { + crashReports.forEach { file -> + CrashReportItem( + file = file, + dateFormat = displayDateFormat, + onShare = { + HapticUtil.performVirtualKeyHaptic(view) + try { + val uri = + FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file, + ) + val shareIntent = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra( + Intent.EXTRA_SUBJECT, + "Essentials Crash Log - ${file.name}", + ) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(shareIntent, null)) + } catch (e: Exception) { + Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() + } + }, + ) + } + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + LogManager.clearAllCrashReports(context) + crashReports = emptyList() + Toast.makeText( + context, + context.getString(R.string.toast_crash_logs_cleared), + Toast.LENGTH_SHORT, + ).show() + }, + modifier = + Modifier + .fillMaxWidth() + .height(44.dp), + shape = ButtonDefaults.shape, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + contentPadding = PaddingValues(0.dp), + ) { + Icon( + painter = painterResource(R.drawable.rounded_delete_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.btn_clear_all_crash_logs), + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + } +} + +@Composable +private fun CrashReportItem( + file: File, + dateFormat: SimpleDateFormat, + onShare: () -> Unit, +) { + val formattedDate = remember(file.lastModified()) { + dateFormat.format(Date(file.lastModified())) + } + val fileSizeKb = remember(file.length()) { + "${(file.length() / 1024).coerceAtLeast(1)} KB" + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = file.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "$formattedDate • $fileSizeKb", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + IconButton( + onClick = onShare, + ) { + Icon( + painter = painterResource(R.drawable.rounded_share_24), + contentDescription = stringResource(R.string.action_share), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp), + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt b/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt index 29fdf6ae3..3a44d1076 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/LogManager.kt @@ -28,7 +28,7 @@ object LogManager { private val logBuffer = LinkedList() private const val CRASH_LOG_FILENAME = "last_crash.log" private const val CRASH_REPORTS_DIR = "crash_reports" - private const val MAX_CRASH_REPORTS = 10 + private const val MAX_CRASH_REPORTS = 5 private var lastCrashLog: String? = null private val isInitialized = AtomicBoolean(false) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9bffe952..4073fbe35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -916,8 +916,9 @@ Auto Essentials crashed, report saved and sent Simulate crash - Share latest crash log - Clear crash logs + Crash logs + View and share recent crash reports + Clear all Crash logs cleared No crash logs found Welcome to Essentials From dd13dbbd7c9b7b0e03e2011c14b2ecd51456adfc Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 22:00:53 +0530 Subject: [PATCH 03/17] feat: Updated developer options UI --- .../ui/activities/SettingsActivity.kt | 152 +++++++++++++----- .../rounded_vertical_align_bottom_24.xml | 10 ++ .../rounded_vertical_align_top_24.xml | 10 ++ app/src/main/res/values/strings.xml | 11 ++ 4 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_vertical_align_bottom_24.xml create mode 100644 app/src/main/res/drawable/rounded_vertical_align_top_24.xml diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index 7f762225b..e89f98dd4 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -41,6 +41,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -1172,10 +1173,12 @@ fun SettingsContent( color = MaterialTheme.colorScheme.surfaceBright, shape = Shapes.extraSmall, ).padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + verticalAlignment = Alignment.CenterVertically, ) { Button( onClick = { + HapticUtil.performVirtualKeyHaptic(view) val timeStamp = SimpleDateFormat( "yyyyMMdd_HHmmss", @@ -1183,21 +1186,42 @@ fun SettingsContent( ).format(Date()) exportLauncher.launch("essentials_config_$timeStamp.json") }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .defaultMinSize(minHeight = 44.dp), + shape = ButtonGroupDefaults.connectedLeadingButtonShapes().shape, ) { - Text("Export Config") + Icon( + painter = painterResource(R.drawable.rounded_vertical_align_bottom_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.btn_export_config)) } Button( onClick = { + HapticUtil.performVirtualKeyHaptic(view) importLauncher.launch(arrayOf("application/json")) }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .defaultMinSize(minHeight = 44.dp), + shape = ButtonGroupDefaults.connectedTrailingButtonShapes().shape, ) { - Text("Import Config") + Icon( + painter = painterResource(R.drawable.rounded_vertical_align_top_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.btn_import_config)) } } - Row( + Column( modifier = Modifier .fillMaxWidth() @@ -1205,49 +1229,58 @@ fun SettingsContent( color = MaterialTheme.colorScheme.surfaceBright, shape = Shapes.extraSmall, ).padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) viewModel.resetOnboarding(context) - Toast.makeText(context, "Onboarding reset", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_onboarding_reset), Toast.LENGTH_SHORT).show() }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 44.dp), colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, ), ) { - Text("Reset onboarding", color = MaterialTheme.colorScheme.onError) + Icon( + painter = painterResource(R.drawable.rounded_refresh_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.btn_reset_onboarding)) } Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) viewModel.resetUpdateNote(context) - Toast.makeText(context, "Update note reset", Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.toast_update_note_reset), Toast.LENGTH_SHORT).show() }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 44.dp), colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, ), ) { - Text("Reset update note", color = MaterialTheme.colorScheme.onError) + Icon( + painter = painterResource(R.drawable.rounded_refresh_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.btn_reset_update_note)) } - } - Row( - modifier = - Modifier - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = Shapes.extraSmall, - ).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { Button( onClick = { HapticUtil.performVirtualKeyHaptic(view) @@ -1257,19 +1290,20 @@ fun SettingsContent( modifier = Modifier .fillMaxWidth() - .height(40.dp), - shape = ButtonDefaults.shape, + .defaultMinSize(minHeight = 44.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError, ), - contentPadding = PaddingValues(0.dp), ) { - Text( - text = stringResource(R.string.btn_clear_search_history), - style = MaterialTheme.typography.labelLarge, + Icon( + painter = painterResource(R.drawable.rounded_delete_24), + contentDescription = null, + modifier = Modifier.size(20.dp), ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.btn_clear_search_history)) } } @@ -1290,8 +1324,7 @@ fun SettingsContent( .background( color = MaterialTheme.colorScheme.surfaceBright, shape = Shapes.extraSmall, - ).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + ).padding(16.dp), ) { Button( onClick = { @@ -1301,15 +1334,20 @@ fun SettingsContent( modifier = Modifier .fillMaxWidth() - .height(40.dp), + .defaultMinSize(minHeight = 44.dp), shape = ButtonDefaults.shape, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ), - contentPadding = PaddingValues(0.dp), ) { + Icon( + painter = painterResource(R.drawable.rounded_bug_report_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(R.string.simulate_crash), style = MaterialTheme.typography.labelLarge, @@ -1487,30 +1525,60 @@ fun SettingsContent( HapticUtil.performUIHaptic(view) viewModel.triggerWallpaperUpdate("desktop") }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .defaultMinSize(minHeight = 44.dp), + shape = ButtonGroupDefaults.connectedLeadingButtonShapes().shape, enabled = !isTriggering, ) { - Text("Desktop") + Icon( + painter = painterResource(R.drawable.rounded_laptop_mac_24), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(stringResource(R.string.btn_wallpaper_desktop)) } Button( onClick = { HapticUtil.performUIHaptic(view) viewModel.triggerWallpaperUpdate("both") }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .defaultMinSize(minHeight = 44.dp), + shape = ButtonGroupDefaults.connectedMiddleButtonShapes().shape, enabled = !isTriggering, ) { - Text("Both") + Icon( + painter = painterResource(R.drawable.rounded_devices_24), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(stringResource(R.string.btn_wallpaper_both)) } Button( onClick = { HapticUtil.performUIHaptic(view) viewModel.triggerWallpaperUpdate("mobile") }, - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .defaultMinSize(minHeight = 44.dp), + shape = ButtonGroupDefaults.connectedTrailingButtonShapes().shape, enabled = !isTriggering, ) { - Text("Mobile") + Icon( + painter = painterResource(R.drawable.rounded_mobile_24), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(stringResource(R.string.btn_wallpaper_mobile)) } } diff --git a/app/src/main/res/drawable/rounded_vertical_align_bottom_24.xml b/app/src/main/res/drawable/rounded_vertical_align_bottom_24.xml new file mode 100644 index 000000000..caeec70b4 --- /dev/null +++ b/app/src/main/res/drawable/rounded_vertical_align_bottom_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/rounded_vertical_align_top_24.xml b/app/src/main/res/drawable/rounded_vertical_align_top_24.xml new file mode 100644 index 000000000..a62c31dc1 --- /dev/null +++ b/app/src/main/res/drawable/rounded_vertical_align_top_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4073fbe35..587a5345d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,6 +921,17 @@ Clear all Crash logs cleared No crash logs found + Export Config + Import Config + Reset onboarding + Reset update note + Onboarding reset + Update note reset + Developer Options + Wallpaper Update + Desktop + Both + Mobile Welcome to Essentials A Toolbox for Android Nerds by sameerasw.com From 53dc3ce2338c038165e8711d858bddf3c539cf1a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 22:12:18 +0530 Subject: [PATCH 04/17] feat: move permissions in settings to top --- .../ui/activities/SettingsActivity.kt | 184 +++++++++--------- .../ui/core/cards/PermissionCard.kt | 3 +- 2 files changed, 98 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index e89f98dd4..716a8f95b 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -564,7 +564,7 @@ fun SettingsContent( ) { val view = LocalView.current - // Help Section + // Help & Permissions Section RoundedCardContainer { IconToggleItem( iconRes = R.drawable.rounded_help_24, @@ -579,6 +579,101 @@ fun SettingsContent( .fillMaxWidth() .height(72.dp), ) + + Row( + modifier = + Modifier + .fillMaxWidth() + .height(72.dp) + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall, + ) + .onGloballyPositioned { coordinates -> + permissionsSectionY = coordinates.positionInParent().y + } + .clickable { isPermissionsExpanded = !isPermissionsExpanded } + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_shield_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.settings_section_permissions), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + Icon( + painter = + painterResource( + id = if (isPermissionsExpanded) R.drawable.rounded_keyboard_arrow_up_24 else R.drawable.rounded_keyboard_arrow_down_24, + ), + contentDescription = if (isPermissionsExpanded) "Collapse" else "Expand", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + AnimatedVisibility( + visible = isPermissionsExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + val permissionItems = + remember( + isAccessibilityEnabled, + isWriteSecureSettingsEnabled, + isRootEnabled, + isRootPermissionGranted, + isShizukuPermissionGranted, + isShizukuAvailable, + isReadPhoneStateEnabled, + isPostNotificationsEnabled, + isOverlayPermissionGranted, + isNotificationListenerEnabled, + isWriteSettingsEnabled, + isNotificationPolicyAccessGranted, + isDefaultBrowserSet, + isUsageStatsPermissionGranted, + isLocationPermissionGranted, + isBackgroundLocationPermissionGranted, + isDeviceAdminEnabled, + isCalendarPermissionGranted, + ) { + PermissionUIHelper.getAllPermissionItems(context, viewModel, context as? ComponentActivity) + } + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + permissionItems.forEach { item -> + PermissionCard( + iconRes = item.iconRes, + title = item.title, + dependentFeatures = item.dependentFeatures, + actionLabel = item.actionLabel ?: R.string.perm_action_grant, + isGranted = item.isGranted, + onActionClick = { item.action?.invoke() }, + secondaryActionLabel = item.secondaryActionLabel, + onSecondaryActionClick = item.secondaryAction, + shizukuActionLabel = item.shizukuActionLabel, + shizukuActionEnabled = item.shizukuActionEnabled, + onShizukuActionClick = item.shizukuAction, + instructions = item.instructions, + description = item.description, + ) + } + } + } } // Updates Section @@ -1046,93 +1141,6 @@ fun SettingsContent( ) } - Spacer(modifier = Modifier.height(16.dp)) - - // Permissions Section - Row( - modifier = - Modifier - .fillMaxWidth() - .onGloballyPositioned { coordinates -> - permissionsSectionY = coordinates.positionInParent().y - } - .clickable { isPermissionsExpanded = !isPermissionsExpanded } - .padding(start = 16.dp, top = 16.dp, bottom = 8.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(R.string.settings_section_permissions), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Icon( - painter = - painterResource( - id = if (isPermissionsExpanded) R.drawable.rounded_keyboard_arrow_up_24 else R.drawable.rounded_keyboard_arrow_down_24, - ), - contentDescription = if (isPermissionsExpanded) "Collapse" else "Expand", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - AnimatedVisibility( - visible = isPermissionsExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - val permissionItems = - remember( - isAccessibilityEnabled, - isWriteSecureSettingsEnabled, - isRootEnabled, - isRootPermissionGranted, - isShizukuPermissionGranted, - isShizukuAvailable, - isReadPhoneStateEnabled, - isPostNotificationsEnabled, - isOverlayPermissionGranted, - isNotificationListenerEnabled, - isWriteSettingsEnabled, - isNotificationPolicyAccessGranted, - isDefaultBrowserSet, - isUsageStatsPermissionGranted, - isLocationPermissionGranted, - isBackgroundLocationPermissionGranted, - isDeviceAdminEnabled, - isCalendarPermissionGranted, - ) { - PermissionUIHelper.getAllPermissionItems(context, viewModel, context as? ComponentActivity) - } - - RoundedCardContainer { - permissionItems.forEach { item -> - PermissionCard( - iconRes = item.iconRes, - title = item.title, - dependentFeatures = item.dependentFeatures, - actionLabel = item.actionLabel ?: R.string.perm_action_grant, - isGranted = item.isGranted, - onActionClick = { item.action?.invoke() }, - secondaryActionLabel = item.secondaryActionLabel, - onSecondaryActionClick = item.secondaryAction, - shizukuActionLabel = item.shizukuActionLabel, - shizukuActionEnabled = item.shizukuActionEnabled, - onShizukuActionClick = item.shizukuAction, - instructions = item.instructions, - description = item.description, - ) - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - MadebySameeraswCard() Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt index 0174f0eac..9cdff85c9 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.translation.TranslationManager import com.sameerasw.essentials.translation.ui.TranslationBottomSheet +import com.sameerasw.essentials.ui.theme.Shapes import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.utils.HapticUtil @@ -119,7 +120,7 @@ fun PermissionCard( Card( modifier = modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.extraSmall, + shape = Shapes.extraSmall, colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceBright, From 50b781cbe9fceb78117262809d1a9f03b7ba0107 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 22:17:21 +0530 Subject: [PATCH 05/17] fix: settings spacing --- .../com/sameerasw/essentials/ui/activities/SettingsActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index 716a8f95b..f25e6d619 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -1141,6 +1141,8 @@ fun SettingsContent( ) } + Spacer(modifier = Modifier.height(16.dp)) + MadebySameeraswCard() Spacer(modifier = Modifier.height(4.dp)) From cb203555e6badf230ef96b79c944cc8eb29a6b3a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 22:25:25 +0530 Subject: [PATCH 06/17] refactor: reorganize Settings --- .../ui/activities/SettingsActivity.kt | 293 ++++++++++-------- app/src/main/res/values/strings.xml | 3 + 2 files changed, 159 insertions(+), 137 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt index f25e6d619..cde55c3de 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/SettingsActivity.kt @@ -564,7 +564,7 @@ fun SettingsContent( ) { val view = LocalView.current - // Help & Permissions Section + // Help & Guides RoundedCardContainer { IconToggleItem( iconRes = R.drawable.rounded_help_24, @@ -579,104 +579,9 @@ fun SettingsContent( .fillMaxWidth() .height(72.dp), ) - - Row( - modifier = - Modifier - .fillMaxWidth() - .height(72.dp) - .background( - color = MaterialTheme.colorScheme.surfaceBright, - shape = Shapes.extraSmall, - ) - .onGloballyPositioned { coordinates -> - permissionsSectionY = coordinates.positionInParent().y - } - .clickable { isPermissionsExpanded = !isPermissionsExpanded } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.rounded_shield_24), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(R.string.settings_section_permissions), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - Icon( - painter = - painterResource( - id = if (isPermissionsExpanded) R.drawable.rounded_keyboard_arrow_up_24 else R.drawable.rounded_keyboard_arrow_down_24, - ), - contentDescription = if (isPermissionsExpanded) "Collapse" else "Expand", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - AnimatedVisibility( - visible = isPermissionsExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - val permissionItems = - remember( - isAccessibilityEnabled, - isWriteSecureSettingsEnabled, - isRootEnabled, - isRootPermissionGranted, - isShizukuPermissionGranted, - isShizukuAvailable, - isReadPhoneStateEnabled, - isPostNotificationsEnabled, - isOverlayPermissionGranted, - isNotificationListenerEnabled, - isWriteSettingsEnabled, - isNotificationPolicyAccessGranted, - isDefaultBrowserSet, - isUsageStatsPermissionGranted, - isLocationPermissionGranted, - isBackgroundLocationPermissionGranted, - isDeviceAdminEnabled, - isCalendarPermissionGranted, - ) { - PermissionUIHelper.getAllPermissionItems(context, viewModel, context as? ComponentActivity) - } - - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - permissionItems.forEach { item -> - PermissionCard( - iconRes = item.iconRes, - title = item.title, - dependentFeatures = item.dependentFeatures, - actionLabel = item.actionLabel ?: R.string.perm_action_grant, - isGranted = item.isGranted, - onActionClick = { item.action?.invoke() }, - secondaryActionLabel = item.secondaryActionLabel, - onSecondaryActionClick = item.secondaryAction, - shizukuActionLabel = item.shizukuActionLabel, - shizukuActionEnabled = item.shizukuActionEnabled, - onShizukuActionClick = item.shizukuAction, - instructions = item.instructions, - description = item.description, - ) - } - } - } } - // Updates Section + // Updates Text( text = "Updates", style = MaterialTheme.typography.titleMedium, @@ -762,21 +667,43 @@ fun SettingsContent( Spacer(modifier = Modifier.height(8.dp)) - // App Settings Section + // Customizations Text( - text = "App Settings", + text = stringResource(R.string.settings_section_customizations), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), color = MaterialTheme.colorScheme.onSurfaceVariant, ) RoundedCardContainer { + val selectedAppIcon by viewModel.selectedAppIcon + AppIconPicker( + selectedIcon = selectedAppIcon, + onIconSelected = { viewModel.setAppIcon(it, context) }, + onIconSelectedWithPosition = onAppIconSelectedWithPosition, + ) + val appLanguage by viewModel.appLanguage LanguagePicker( selectedLanguageCode = appLanguage, onLanguageSelected = { viewModel.setAppLanguage(it) }, ) + val defaultTab by viewModel.defaultTab + val availableTabs = remember { DIYTabs.entries } + DefaultTabPicker( + selectedTab = defaultTab, + onTabSelected = { viewModel.setDefaultTab(it, context) }, + options = availableTabs, + ) + + IconToggleItem( + iconRes = R.drawable.rounded_touch_app_24, + title = stringResource(R.string.setting_swipe_tabs_title), + isChecked = viewModel.isSwipeTabsEnabled.value, + onCheckedChange = { viewModel.setSwipeTabsEnabled(it) }, + ) + IconToggleItem( iconRes = R.drawable.rounded_mobile_vibrate_24, title = "Haptic Feedback", @@ -834,39 +761,114 @@ fun SettingsContent( isChecked = viewModel.isOnlineHelpMediaEnabled.value, onCheckedChange = { viewModel.setOnlineHelpMediaEnabled(it, context) }, ) + } - CrashReportingPicker( - selectedMode = sentryMode, - onModeSelected = { viewModel.setSentryReportMode(it, context) }, - ) - - val defaultTab by viewModel.defaultTab + Spacer(modifier = Modifier.height(8.dp)) - val availableTabs = remember { DIYTabs.entries } - DefaultTabPicker( - selectedTab = defaultTab, - onTabSelected = { viewModel.setDefaultTab(it, context) }, - options = availableTabs, - ) + // Permissions + Text( + text = stringResource(R.string.settings_section_permissions), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) - val selectedAppIcon by viewModel.selectedAppIcon - AppIconPicker( - selectedIcon = selectedAppIcon, - onIconSelected = { viewModel.setAppIcon(it, context) }, - onIconSelectedWithPosition = onAppIconSelectedWithPosition, - ) + RoundedCardContainer { + Row( + modifier = + Modifier + .fillMaxWidth() + .height(72.dp) + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = Shapes.extraSmall, + ) + .onGloballyPositioned { coordinates -> + permissionsSectionY = coordinates.positionInParent().y + } + .clickable { isPermissionsExpanded = !isPermissionsExpanded } + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_shield_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.settings_permissions_all), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + Icon( + painter = + painterResource( + id = if (isPermissionsExpanded) R.drawable.rounded_keyboard_arrow_up_24 else R.drawable.rounded_keyboard_arrow_down_24, + ), + contentDescription = if (isPermissionsExpanded) "Collapse" else "Expand", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } - IconToggleItem( - iconRes = R.drawable.rounded_touch_app_24, - title = stringResource(R.string.setting_swipe_tabs_title), - isChecked = viewModel.isSwipeTabsEnabled.value, - onCheckedChange = { viewModel.setSwipeTabsEnabled(it) }, - ) - } + AnimatedVisibility( + visible = isPermissionsExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + val permissionItems = + remember( + isAccessibilityEnabled, + isWriteSecureSettingsEnabled, + isRootEnabled, + isRootPermissionGranted, + isShizukuPermissionGranted, + isShizukuAvailable, + isReadPhoneStateEnabled, + isPostNotificationsEnabled, + isOverlayPermissionGranted, + isNotificationListenerEnabled, + isWriteSettingsEnabled, + isNotificationPolicyAccessGranted, + isDefaultBrowserSet, + isUsageStatsPermissionGranted, + isLocationPermissionGranted, + isBackgroundLocationPermissionGranted, + isDeviceAdminEnabled, + isCalendarPermissionGranted, + ) { + PermissionUIHelper.getAllPermissionItems(context, viewModel, context as? ComponentActivity) + } - Spacer(modifier = Modifier.height(8.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + permissionItems.forEach { item -> + PermissionCard( + iconRes = item.iconRes, + title = item.title, + dependentFeatures = item.dependentFeatures, + actionLabel = item.actionLabel ?: R.string.perm_action_grant, + isGranted = item.isGranted, + onActionClick = { item.action?.invoke() }, + secondaryActionLabel = item.secondaryActionLabel, + onSecondaryActionClick = item.secondaryAction, + shizukuActionLabel = item.shizukuActionLabel, + shizukuActionEnabled = item.shizukuActionEnabled, + onShizukuActionClick = item.shizukuAction, + instructions = item.instructions, + description = item.description, + ) + } + } + } - RoundedCardContainer { IconToggleItem( iconRes = R.drawable.rounded_numbers_24, title = stringResource(R.string.setting_use_root_title), @@ -949,6 +951,33 @@ fun SettingsContent( onCheckedChange = { viewModel.setUseUsageAccess(it, context) }, ) + if (isGenAISupported) { + IconToggleItem( + iconRes = R.drawable.rounded_auto_awesome_24, + title = stringResource(R.string.settings_genai_automation_title), + description = stringResource(R.string.settings_genai_automation_desc), + isChecked = isGenAIAutomationEnabled, + onCheckedChange = { viewModel.setGenAIAutomationEnabled(it, context) }, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // More + Text( + text = stringResource(R.string.settings_section_more), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + RoundedCardContainer { + CrashReportingPicker( + selectedMode = sentryMode, + onModeSelected = { viewModel.setSentryReportMode(it, context) }, + ) + IconToggleItem( iconRes = R.drawable.rounded_release_alert_24, title = stringResource(R.string.setting_enable_unsupported_features_title), @@ -962,16 +991,6 @@ fun SettingsContent( } }, ) - - if (isGenAISupported) { - IconToggleItem( - iconRes = R.drawable.rounded_auto_awesome_24, - title = stringResource(R.string.settings_genai_automation_title), - description = stringResource(R.string.settings_genai_automation_desc), - isChecked = isGenAIAutomationEnabled, - onCheckedChange = { viewModel.setGenAIAutomationEnabled(it, context) }, - ) - } } Spacer(modifier = Modifier.height(16.dp)) @@ -1127,7 +1146,7 @@ fun SettingsContent( } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(24.dp)) RoundedCardContainer { FeatureCard( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 587a5345d..1652b8526 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2441,4 +2441,7 @@ Enable Admin Set as Default Auto-Grant + Customizations + All permissions + More From 4efaef94b54fed727eb9ad40375f8d9c8702edc0 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 9 Sep 2026 23:56:45 +0530 Subject: [PATCH 07/17] refactor: update AOD wallpaper extraction to require Android 14+ and catch Throwable exceptions --- .../services/handlers/AodWallpaperOverlayHandler.kt | 4 ++-- .../java/com/sameerasw/essentials/viewmodels/MainViewModel.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AodWallpaperOverlayHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AodWallpaperOverlayHandler.kt index 978ff8bd8..c210a6e9b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AodWallpaperOverlayHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AodWallpaperOverlayHandler.kt @@ -686,7 +686,7 @@ class AodWallpaperOverlayHandler( val wallpaperManager = WallpaperManager.getInstance(service) val drawable: Drawable? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { wallpaperManager.getDrawable(WallpaperManager.FLAG_LOCK) ?: wallpaperManager.drawable } else { @@ -694,7 +694,7 @@ class AodWallpaperOverlayHandler( } drawableToBitmap(drawable) - } catch (e: Exception) { + } catch (e: Throwable) { Log.e("AodWallpaperOverlay", "Error extracting current wallpaper", e) null } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 3984261e9..27696da4d 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -7183,7 +7183,7 @@ class MainViewModel : ViewModel() { if (PermissionUtils.hasManageExternalStoragePermission(context)) { val wallpaperManager = android.app.WallpaperManager.getInstance(context) val drawable = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { wallpaperManager.getDrawable(android.app.WallpaperManager.FLAG_LOCK) ?: wallpaperManager.drawable } else { @@ -7207,7 +7207,7 @@ class MainViewModel : ViewModel() { } } } - } catch (_: Exception) { + } catch (_: Throwable) { } } } From 5cb9e012a18c1fc18465cc41e5cfab9bc2025a4c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:25:34 +0530 Subject: [PATCH 08/17] fix: allow lower controls for CTS height --- .../essentials/ui/features/system/NavigationSettingsUI.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt index 2af69d71a..484215ce3 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt @@ -279,8 +279,8 @@ fun NavigationSettingsUI( title = stringResource(R.string.feat_circle_to_search_gesture_height_title), value = viewModel.circleToSearchGestureHeight.floatValue, onValueChange = { viewModel.setCircleToSearchGestureHeight(it) }, - valueRange = 24f..120f, - increment = 4f, + valueRange = 14f..120f, + increment = 2f, iconRes = R.drawable.rounded_border_bottom_24, description = stringResource(R.string.feat_circle_to_search_gesture_height_desc), valueFormatter = { "${it.toInt()} dp" }, From b806ad216879ec6b2422b75b6c7c07f771c5b7f4 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:32:48 +0530 Subject: [PATCH 09/17] fix: enforce accessibility dependency across Smart Pixels, App Lock, and Circle to Search features #940 --- .../domain/registry/FeatureRegistry.kt | 2 +- .../features/display/SmartPixelsSettingsUI.kt | 29 +++++++++++- .../ui/features/security/AppLockSettingsUI.kt | 47 ++++++++++--------- .../features/system/NavigationSettingsUI.kt | 10 ++-- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 4a5f606d8..086cb56e5 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -1454,7 +1454,7 @@ object FeatureRegistry { context: Context, ) = ( if (viewModel.isUseUsageAccess.value) { - viewModel.isUsageStatsPermissionGranted.value + viewModel.isUsageStatsPermissionGranted.value && viewModel.isAccessibilityEnabled.value } else { viewModel.isAccessibilityEnabled.value } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt index 2bb2fbbfe..3dccf64d2 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 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 @@ -27,8 +28,10 @@ import com.sameerasw.essentials.R import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem import com.sameerasw.essentials.ui.core.cards.IconToggleItem import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.PermissionUIHelper import com.sameerasw.essentials.viewmodels.MainViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -40,6 +43,24 @@ fun SmartPixelsSettingsUI( ) { val context = LocalContext.current val view = LocalView.current + val isAccessibilityEnabled = viewModel.isAccessibilityEnabled.value + var showPermissionSheet by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + viewModel.check(context) + } + + if (showPermissionSheet) { + val permissionItems = PermissionUIHelper.getPermissionItems(listOf("ACCESSIBILITY"), context, viewModel) + PermissionsBottomSheet( + onDismissRequest = { + showPermissionSheet = false + viewModel.check(context) + }, + featureTitle = R.string.feat_smart_pixels_title, + permissions = permissionItems, + ) + } Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { RoundedCardContainer( @@ -52,8 +73,14 @@ fun SmartPixelsSettingsUI( isChecked = viewModel.isSmartPixelsEnabled.value, onCheckedChange = { checked -> HapticUtil.performUIHaptic(view) - viewModel.setSmartPixelsEnabled(context, checked) + if (isAccessibilityEnabled) { + viewModel.setSmartPixelsEnabled(context, checked) + } else { + showPermissionSheet = true + } }, + enabled = isAccessibilityEnabled, + onDisabledClick = { showPermissionSheet = true }, modifier = Modifier.highlight(highlightSetting == "smart_pixels_enable_toggle"), ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/security/AppLockSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/security/AppLockSettingsUI.kt index f066f9aea..3406bd8d2 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/security/AppLockSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/security/AppLockSettingsUI.kt @@ -61,7 +61,7 @@ fun AppLockSettingsUI( val isUsageStatsPermissionGranted by viewModel.isUsageStatsPermissionGranted val canEnableAppLock = - if (isUseUsageAccess) isUsageStatsPermissionGranted else isAccessibilityEnabled + if (isUseUsageAccess) isUsageStatsPermissionGranted && isAccessibilityEnabled else isAccessibilityEnabled val delayLabels = listOf( @@ -188,27 +188,32 @@ fun AppLockSettingsUI( } if (showPermissionSheet) { - val permissionItem = + val accessibilityItem = + PermissionItem( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = R.string.perm_accessibility_title, + description = R.string.perm_accessibility_desc_common, + dependentFeatures = listOf(R.string.feat_app_lock_title), + actionLabel = if (isAccessibilityEnabled) R.string.perm_action_granted else R.string.perm_action_grant, + action = { PermissionUtils.openAccessibilitySettings(context) }, + isGranted = isAccessibilityEnabled, + ) + + val permissionsList = if (isUseUsageAccess) { - PermissionItem( - iconRes = R.drawable.rounded_data_usage_24, - title = R.string.perm_usage_stats_title, - description = R.string.perm_usage_stats_desc_app_lock, - dependentFeatures = listOf(R.string.feat_app_lock_title), - actionLabel = if (isUsageStatsPermissionGranted) R.string.perm_action_granted else R.string.perm_action_grant, - action = { PermissionUtils.openUsageStatsSettings(context) }, - isGranted = isUsageStatsPermissionGranted, - ) + val usageStatsItem = + PermissionItem( + iconRes = R.drawable.rounded_data_usage_24, + title = R.string.perm_usage_stats_title, + description = R.string.perm_usage_stats_desc_app_lock, + dependentFeatures = listOf(R.string.feat_app_lock_title), + actionLabel = if (isUsageStatsPermissionGranted) R.string.perm_action_granted else R.string.perm_action_grant, + action = { PermissionUtils.openUsageStatsSettings(context) }, + isGranted = isUsageStatsPermissionGranted, + ) + listOf(accessibilityItem, usageStatsItem) } else { - PermissionItem( - iconRes = R.drawable.rounded_settings_accessibility_24, - title = R.string.perm_accessibility_title, - description = R.string.perm_accessibility_desc_common, - dependentFeatures = listOf(R.string.feat_app_lock_title), - actionLabel = if (isAccessibilityEnabled) R.string.perm_action_granted else R.string.perm_action_grant, - action = { PermissionUtils.openAccessibilitySettings(context) }, - isGranted = isAccessibilityEnabled, - ) + listOf(accessibilityItem) } PermissionsBottomSheet( @@ -218,7 +223,7 @@ fun AppLockSettingsUI( viewModel.check(context) }, featureTitle = R.string.feat_app_lock_title, - permissions = listOf(permissionItem), + permissions = permissionsList, ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt index 484215ce3..3883ea02e 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/system/NavigationSettingsUI.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -38,6 +39,7 @@ import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.sheets.PermissionItem import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet import com.sameerasw.essentials.ui.modifiers.highlight +import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.PermissionUtils import com.sameerasw.essentials.viewmodels.MainViewModel @@ -48,6 +50,7 @@ fun NavigationSettingsUI( highlightSetting: String? = null, ) { val context = LocalContext.current + val view = LocalView.current var requestingPermissionFor by remember { mutableStateOf(PermissionModule.NONE) } if (requestingPermissionFor != PermissionModule.NONE) { @@ -233,17 +236,16 @@ fun NavigationSettingsUI( description = stringResource(R.string.feat_circle_to_search_gesture_desc), isChecked = viewModel.isCircleToSearchGestureEnabled.value, onCheckedChange = { enabled -> + HapticUtil.performUIHaptic(view) if (isShellGranted && isAccessibilityEnabled) { viewModel.setCircleToSearchGestureEnabled(enabled, context) } else { requestingPermissionFor = PermissionModule.CIRCLE_TO_SEARCH } }, - enabled = true, + enabled = isShellGranted && isAccessibilityEnabled, onDisabledClick = { - if (!isShellGranted || !isAccessibilityEnabled) { - requestingPermissionFor = PermissionModule.CIRCLE_TO_SEARCH - } + requestingPermissionFor = PermissionModule.CIRCLE_TO_SEARCH }, iconRes = R.drawable.rounded_touch_app_24, modifier = Modifier.highlight(highlightSetting == "circle_to_search_gesture_toggle"), From 61a9fb9d06cc5b65d8b4c80e1b3062d971a2ac52 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:39:10 +0530 Subject: [PATCH 10/17] refactor: compact permission cards in bottom sheet --- .../ui/core/cards/PermissionCard.kt | 32 +++++++++++-------- .../ui/core/sheets/PermissionsBottomSheet.kt | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt index 9cdff85c9..1bfa9dc5f 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt @@ -160,21 +160,25 @@ fun PermissionCard( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(modifier = Modifier.height(8.dp)) } - Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) - Spacer(modifier = Modifier.height(4.dp)) - dependentFeatures.forEach { f -> - val resolvedFeature = - when (f) { - is Int -> stringResource(id = f) - is String -> f - else -> "" - } - Text( - text = "• $resolvedFeature", - style = MaterialTheme.typography.bodyMedium, - ) + if (dependentFeatures.isNotEmpty()) { + if (resolvedDescription != null) { + Spacer(modifier = Modifier.height(8.dp)) + } + Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) + Spacer(modifier = Modifier.height(4.dp)) + dependentFeatures.forEach { f -> + val resolvedFeature = + when (f) { + is Int -> stringResource(id = f) + is String -> f + else -> "" + } + Text( + text = "• $resolvedFeature", + style = MaterialTheme.typography.bodyMedium, + ) + } } } }, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt index af4067ad5..1ed7a9fcb 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt @@ -87,7 +87,7 @@ fun PermissionsBottomSheet( PermissionCard( iconRes = perm.iconRes, title = perm.title, - dependentFeatures = perm.dependentFeatures, + dependentFeatures = emptyList(), actionLabel = perm.actionLabel ?: R.string.perm_action_enable, isGranted = perm.isGranted, onActionClick = { perm.action?.invoke() }, From 95b0a760ac10c173765d17f05480af32a928bd8a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:41:45 +0530 Subject: [PATCH 11/17] feat: hide granted permissions by default in the sheet --- .../ui/core/sheets/PermissionsBottomSheet.kt | 94 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 + 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt index 1ed7a9fcb..16c627050 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt @@ -18,13 +18,25 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +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.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.buttons.ListExpandToggleButton import com.sameerasw.essentials.ui.core.cards.PermissionCard import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.HapticUtil data class PermissionItem( val iconRes: Int, @@ -50,6 +62,12 @@ fun PermissionsBottomSheet( permissions: List, onHelpClick: () -> Unit = {}, ) { + val view = LocalView.current + var showGranted by remember { mutableStateOf(false) } + + val pendingPermissions = permissions.filter { !it.isGranted } + val grantedPermissions = permissions.filter { it.isGranted } + val resolvedTitle = when (featureTitle) { is Int -> stringResource(id = featureTitle) @@ -82,23 +100,65 @@ fun PermissionsBottomSheet( ) } - RoundedCardContainer { - permissions.forEach { perm -> - PermissionCard( - iconRes = perm.iconRes, - title = perm.title, - dependentFeatures = emptyList(), - actionLabel = perm.actionLabel ?: R.string.perm_action_enable, - isGranted = perm.isGranted, - onActionClick = { perm.action?.invoke() }, - secondaryActionLabel = perm.secondaryActionLabel, - onSecondaryActionClick = { perm.secondaryAction?.invoke() }, - shizukuActionLabel = perm.shizukuActionLabel, - shizukuActionEnabled = perm.shizukuActionEnabled, - onShizukuActionClick = { perm.shizukuAction?.invoke() }, - instructions = perm.instructions, - description = perm.description, - ) + if (pendingPermissions.isNotEmpty()) { + RoundedCardContainer { + pendingPermissions.forEach { perm -> + PermissionCard( + iconRes = perm.iconRes, + title = perm.title, + dependentFeatures = emptyList(), + actionLabel = perm.actionLabel ?: R.string.perm_action_enable, + isGranted = perm.isGranted, + onActionClick = { perm.action?.invoke() }, + secondaryActionLabel = perm.secondaryActionLabel, + onSecondaryActionClick = { perm.secondaryAction?.invoke() }, + shizukuActionLabel = perm.shizukuActionLabel, + shizukuActionEnabled = perm.shizukuActionEnabled, + onShizukuActionClick = { perm.shizukuAction?.invoke() }, + instructions = perm.instructions, + description = perm.description, + ) + } + } + } + + if (grantedPermissions.isNotEmpty()) { + ListExpandToggleButton( + isExpanded = showGranted, + onToggle = { + HapticUtil.performUIHaptic(view) + showGranted = !showGranted + }, + title = R.string.action_hide_granted, + description = R.string.action_show_granted, + expandedText = stringResource(R.string.action_hide_granted), + collapsedText = stringResource(R.string.action_show_granted), + ) + + AnimatedVisibility( + visible = showGranted, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + RoundedCardContainer { + grantedPermissions.forEach { perm -> + PermissionCard( + iconRes = perm.iconRes, + title = perm.title, + dependentFeatures = emptyList(), + actionLabel = perm.actionLabel ?: R.string.perm_action_enable, + isGranted = perm.isGranted, + onActionClick = { perm.action?.invoke() }, + secondaryActionLabel = perm.secondaryActionLabel, + onSecondaryActionClick = { perm.secondaryAction?.invoke() }, + shizukuActionLabel = perm.shizukuActionLabel, + shizukuActionEnabled = perm.shizukuActionEnabled, + onShizukuActionClick = { perm.shizukuAction?.invoke() }, + instructions = perm.instructions, + description = perm.description, + ) + } + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1652b8526..2a4de2336 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2444,4 +2444,6 @@ Customizations All permissions More + Show granted + Hide granted From 33b53f9865f18d2d49977f7e2e9e5c8b1392205a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:46:55 +0530 Subject: [PATCH 12/17] refactor: hide permission descriptions from bottom sheet --- .../ui/core/cards/PermissionCard.kt | 59 ++++++++++--------- .../ui/core/sheets/PermissionsBottomSheet.kt | 4 +- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt index 1bfa9dc5f..7eef092d5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/cards/PermissionCard.kt @@ -152,36 +152,41 @@ fun PermissionCard( modifier = Modifier.size(36.dp), ) }, - supportingContent = { - Column { - if (resolvedDescription != null) { - Text( - text = resolvedDescription, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (dependentFeatures.isNotEmpty()) { - if (resolvedDescription != null) { - Spacer(modifier = Modifier.height(8.dp)) - } - Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) - Spacer(modifier = Modifier.height(4.dp)) - dependentFeatures.forEach { f -> - val resolvedFeature = - when (f) { - is Int -> stringResource(id = f) - is String -> f - else -> "" + supportingContent = + if (resolvedDescription != null || dependentFeatures.isNotEmpty()) { + { + Column { + if (resolvedDescription != null) { + Text( + text = resolvedDescription, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (dependentFeatures.isNotEmpty()) { + if (resolvedDescription != null) { + Spacer(modifier = Modifier.height(8.dp)) } - Text( - text = "• $resolvedFeature", - style = MaterialTheme.typography.bodyMedium, - ) + Text(text = "Required for:", style = MaterialTheme.typography.bodySmall) + Spacer(modifier = Modifier.height(4.dp)) + dependentFeatures.forEach { f -> + val resolvedFeature = + when (f) { + is Int -> stringResource(id = f) + is String -> f + else -> "" + } + Text( + text = "• $resolvedFeature", + style = MaterialTheme.typography.bodyMedium, + ) + } + } } } - } - }, + } else { + null + }, content = { Text( text = resolvedTitle, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt index 16c627050..3073161c9 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt @@ -116,7 +116,7 @@ fun PermissionsBottomSheet( shizukuActionEnabled = perm.shizukuActionEnabled, onShizukuActionClick = { perm.shizukuAction?.invoke() }, instructions = perm.instructions, - description = perm.description, + description = null, ) } } @@ -155,7 +155,7 @@ fun PermissionsBottomSheet( shizukuActionEnabled = perm.shizukuActionEnabled, onShizukuActionClick = { perm.shizukuAction?.invoke() }, instructions = perm.instructions, - description = perm.description, + description = null, ) } } From 9169d0115fc57c28117d086b54b720a2c1408196 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 00:55:23 +0530 Subject: [PATCH 13/17] fix: permission usage and declarations in some features --- .../ui/activities/FeatureSettingsActivity.kt | 3 ++ .../ui/composables/SetupFeatures.kt | 29 +++++++++++++++++++ .../ui/core/sheets/PermissionsBottomSheet.kt | 5 ++-- .../hardware/FlashlightPulseSettingsUI.kt | 27 +++++++++++++++++ .../watch/WatchNotificationSettingsUI.kt | 2 +- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt index 151ee9161..ba39daa21 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt @@ -341,6 +341,8 @@ class FeatureSettingsActivity : AppCompatActivity() { !isOverlayPermissionGranted || !isNotificationLightingAccessibilityEnabled || !isNotificationListenerEnabled + "Flashlight pulse" -> !isNotificationListenerEnabled + "Notification Sync" -> !isNotificationListenerEnabled "Button remap" -> !isAccessibilityEnabled "Pocket mode" -> !isAccessibilityEnabled "Dynamic night light" -> @@ -754,6 +756,7 @@ class FeatureSettingsActivity : AppCompatActivity() { !isOverlayPermissionGranted || !isNotificationLightingAccessibilityEnabled || !isNotificationListenerEnabled + "Flashlight pulse" -> !isNotificationListenerEnabled "Button remap" -> !isAccessibilityEnabled "Dynamic night light" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || 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 bb99bda6f..7d599e94d 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 @@ -489,6 +489,22 @@ fun SetupFeatures( } } + R.string.flashlight_pulse_title -> { + if (!isNotificationListenerEnabled) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = R.string.perm_notif_listener_title, + description = R.string.perm_notif_listener_desc_lighting, + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_LISTENER"), + actionLabel = R.string.perm_action_grant, + action = { viewModel.requestNotificationListenerPermission(context) }, + isGranted = isNotificationListenerEnabled, + ), + ) + } + } + R.string.feat_essentials_on_display_title -> { if (!isAccessibilityEnabled) { missing.add( @@ -768,6 +784,19 @@ fun SetupFeatures( ), ) + R.string.flashlight_pulse_title -> + listOf( + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = R.string.perm_notif_listener_title, + description = R.string.perm_notif_listener_desc_lighting, + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_LISTENER"), + actionLabel = R.string.perm_action_grant, + action = { viewModel.requestNotificationListenerPermission(context) }, + isGranted = isNotificationListenerEnabled, + ), + ) + R.string.feat_essentials_on_display_title -> listOf( PermissionItem( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt index 3073161c9..b1cf0c10d 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt @@ -78,14 +78,15 @@ fun PermissionsBottomSheet( onDismissRequest = onDismissRequest, ) { Column( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) + .padding(bottom = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/FlashlightPulseSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/FlashlightPulseSettingsUI.kt index 9dd70f7db..ec3cbb81a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/FlashlightPulseSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/hardware/FlashlightPulseSettingsUI.kt @@ -38,6 +38,10 @@ import com.sameerasw.essentials.ui.modifiers.highlight import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.viewmodels.MainViewModel +import com.sameerasw.essentials.domain.registry.PermissionRegistry +import com.sameerasw.essentials.ui.core.sheets.PermissionItem +import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FlashlightPulseSettingsUI( @@ -49,6 +53,8 @@ fun FlashlightPulseSettingsUI( val view = LocalView.current var showAppSelectionSheet by remember { mutableStateOf(false) } + var showPermissionsSheet by remember { mutableStateOf(false) } + val isNotificationListenerEnabled by viewModel.isNotificationListenerEnabled Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( @@ -63,6 +69,8 @@ fun FlashlightPulseSettingsUI( iconRes = R.drawable.rounded_flashlight_on_24, title = stringResource(R.string.flashlight_pulse_title), isChecked = viewModel.isFlashlightPulseEnabled.value, + enabled = isNotificationListenerEnabled, + onDisabledClick = { showPermissionsSheet = true }, onCheckedChange = { checked -> viewModel.setFlashlightPulseEnabled(checked, context) }, @@ -158,5 +166,24 @@ fun FlashlightPulseSettingsUI( context = context, ) } + + if (showPermissionsSheet) { + PermissionsBottomSheet( + onDismissRequest = { showPermissionsSheet = false }, + featureTitle = R.string.flashlight_pulse_title, + permissions = + listOf( + PermissionItem( + iconRes = R.drawable.rounded_notifications_unread_24, + title = R.string.perm_notif_listener_title, + description = R.string.perm_notif_listener_desc_lighting, + dependentFeatures = PermissionRegistry.getFeatures("NOTIFICATION_LISTENER"), + actionLabel = R.string.perm_action_grant, + action = { viewModel.requestNotificationListenerPermission(context) }, + isGranted = isNotificationListenerEnabled, + ), + ), + ) + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/watch/WatchNotificationSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/watch/WatchNotificationSettingsUI.kt index 908bde215..583c46457 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/watch/WatchNotificationSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/watch/WatchNotificationSettingsUI.kt @@ -126,7 +126,7 @@ fun WatchNotificationSettingsUI( prefs.edit().putBoolean("watch_notif_sync_enabled", checked).apply() } }, - enabled = true, + enabled = isNotifSyncPermissionGranted, onDisabledClick = { if (!isNotifSyncPermissionGranted) { showNotifPermissionSheet = true From 0e6b5528cf45b062307da9b785e67a507e3fedd7 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 01:05:12 +0530 Subject: [PATCH 14/17] refactor: simplify media loading in help --- .../ui/core/media/FeatureHelpMediaViewer.kt | 55 ++----------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/media/FeatureHelpMediaViewer.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/media/FeatureHelpMediaViewer.kt index a982f3d5c..f7d526185 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/media/FeatureHelpMediaViewer.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/media/FeatureHelpMediaViewer.kt @@ -15,7 +15,6 @@ import android.view.ViewGroup import android.view.WindowManager import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.spring @@ -35,7 +34,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -164,27 +162,13 @@ fun FeatureHelpMediaViewer( } } - Box( - modifier = - modifier - .fillMaxWidth() - .animateContentSize( - animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f), - ), + AnimatedVisibility( + visible = (mediaItem != null && (mediaItem?.type.equals("video", ignoreCase = true) == false || isMediaReady)), + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier.fillMaxWidth(), ) { - if (!hasCheckedMedia) { - Box( - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(9f / 12f) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceBright), - contentAlignment = Alignment.Center, - ) { - LoadingIndicator() - } - } else if (mediaItem != null) { + if (mediaItem != null) { val currentMedia = mediaItem!! val localView = LocalView.current Box( @@ -218,22 +202,6 @@ fun FeatureHelpMediaViewer( modifier = Modifier.fillMaxSize(), ) } - - AnimatedVisibility( - visible = !isMediaReady, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceBright), - contentAlignment = Alignment.Center, - ) { - LoadingIndicator() - } - } } else { SubcomposeAsyncImage( model = @@ -243,17 +211,6 @@ fun FeatureHelpMediaViewer( .build(), contentDescription = null, modifier = Modifier.fillMaxSize(), - loading = { - Box( - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceBright), - contentAlignment = Alignment.Center, - ) { - LoadingIndicator() - } - }, contentScale = ContentScale.Crop, ) } From 69d8660f2861880f72b7548095a2710739222df6 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 01:17:23 +0530 Subject: [PATCH 15/17] feat: add "View All" to permission sheet --- .../ui/core/sheets/PermissionsBottomSheet.kt | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt index b1cf0c10d..66c8aebd7 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/core/sheets/PermissionsBottomSheet.kt @@ -9,30 +9,38 @@ package com.sameerasw.essentials.ui.core.sheets +import android.content.Intent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically 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.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically 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.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R +import com.sameerasw.essentials.SettingsActivity import com.sameerasw.essentials.ui.components.buttons.ListExpandToggleButton import com.sameerasw.essentials.ui.core.cards.PermissionCard import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer @@ -62,42 +70,63 @@ fun PermissionsBottomSheet( permissions: List, onHelpClick: () -> Unit = {}, ) { + val context = LocalContext.current val view = LocalView.current var showGranted by remember { mutableStateOf(false) } val pendingPermissions = permissions.filter { !it.isGranted } val grantedPermissions = permissions.filter { it.isGranted } - val resolvedTitle = - when (featureTitle) { - is Int -> stringResource(id = featureTitle) - is String -> featureTitle - else -> "" - } EssentialsBottomSheet( onDismissRequest = onDismissRequest, ) { Column( modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 4.dp), + .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = - stringResource( - id = R.string.requires_following_permissions, - resolvedTitle, + text = stringResource(R.string.settings_section_permissions), + style = MaterialTheme.typography.titleLarge, + ) + + AssistChip( + onClick = { + HapticUtil.performUIHaptic(view) + onDismissRequest() + val intent = + Intent(context, SettingsActivity::class.java).apply { + putExtra("expand_permissions", true) + } + context.startActivity(intent) + }, + label = { + Text( + text = stringResource(R.string.action_view_all), + style = MaterialTheme.typography.labelMedium, + ) + }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.rounded_chevron_right_24), + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + }, + colors = + AssistChipDefaults.assistChipColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + labelColor = MaterialTheme.colorScheme.onSurface, + trailingIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, ), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.weight(1f), + border = AssistChipDefaults.assistChipBorder(true), ) } From 4d84dbdabfaa51cde9d1d3b0bbb700df85c13582 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 01:23:39 +0530 Subject: [PATCH 16/17] feat: add accessibility requirement and UI constraints to Smart Pixels feature --- .../ui/activities/FeatureSettingsActivity.kt | 2 + .../ui/composables/SetupFeatures.kt | 37 +++++++++++++++++++ .../features/display/SmartPixelsSettingsUI.kt | 7 +++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt index ba39daa21..e2de81552 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/FeatureSettingsActivity.kt @@ -348,6 +348,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Dynamic night light" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || !isWriteSecureSettingsEnabled + "Smart pixels" -> !isAccessibilityEnabled "Snooze system notifications" -> !isNotificationListenerEnabled "Screen locked security" -> !isAccessibilityEnabled || @@ -761,6 +762,7 @@ class FeatureSettingsActivity : AppCompatActivity() { "Dynamic night light" -> (if (viewModel.isUseUsageAccess.value) !viewModel.isUsageStatsPermissionGranted.value else !isAccessibilityEnabled) || !isWriteSecureSettingsEnabled + "Smart pixels" -> !isAccessibilityEnabled "Snooze system notifications" -> !isNotificationListenerEnabled "Screen locked security" -> !isAccessibilityEnabled || 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 7d599e94d..c88ae4d20 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 @@ -460,6 +460,26 @@ fun SetupFeatures( } } + R.string.feat_smart_pixels_title -> { + if (!isAccessibilityEnabled) { + missing.add( + PermissionItem( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = R.string.perm_accessibility_title, + description = R.string.perm_accessibility_desc_common, + dependentFeatures = PermissionRegistry.getFeatures("ACCESSIBILITY"), + actionLabel = R.string.perm_action_enable, + action = { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + }, + isGranted = isAccessibilityEnabled, + ), + ) + } + } + R.string.feat_call_vibrations_title -> { if (!viewModel.isReadPhoneStateEnabled.value) { missing.add( @@ -762,6 +782,23 @@ fun SetupFeatures( ), ) + R.string.feat_smart_pixels_title -> + listOf( + PermissionItem( + iconRes = R.drawable.rounded_settings_accessibility_24, + title = R.string.perm_accessibility_title, + description = R.string.perm_accessibility_desc_common, + dependentFeatures = PermissionRegistry.getFeatures("ACCESSIBILITY"), + actionLabel = R.string.perm_action_enable, + action = { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + }, + isGranted = isAccessibilityEnabled, + ), + ) + R.string.feat_call_vibrations_title -> listOf( PermissionItem( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt index 3dccf64d2..ac089403a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/SmartPixelsSettingsUI.kt @@ -43,7 +43,8 @@ fun SmartPixelsSettingsUI( ) { val context = LocalContext.current val view = LocalView.current - val isAccessibilityEnabled = viewModel.isAccessibilityEnabled.value + val isAccessibilityEnabled by viewModel.isAccessibilityEnabled + val isSmartPixelsEnabled by viewModel.isSmartPixelsEnabled var showPermissionSheet by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -70,7 +71,7 @@ fun SmartPixelsSettingsUI( IconToggleItem( iconRes = R.drawable.rounded_grain_24, title = stringResource(R.string.smart_pixels_enable_title), - isChecked = viewModel.isSmartPixelsEnabled.value, + isChecked = isSmartPixelsEnabled && isAccessibilityEnabled, onCheckedChange = { checked -> HapticUtil.performUIHaptic(view) if (isAccessibilityEnabled) { @@ -106,6 +107,7 @@ fun SmartPixelsSettingsUI( valueRange = 10f..90f, increment = 5f, iconRes = R.drawable.rounded_blur_linear_24, + enabled = isSmartPixelsEnabled && isAccessibilityEnabled, modifier = Modifier.highlight(highlightSetting == "smart_pixels_intensity_slider"), ) } @@ -123,6 +125,7 @@ fun SmartPixelsSettingsUI( HapticUtil.performUIHaptic(view) viewModel.setSmartPixelsDisableOnCastEnabled(context, checked) }, + enabled = isSmartPixelsEnabled && isAccessibilityEnabled, modifier = Modifier.highlight(highlightSetting == "smart_pixels_disable_on_cast_toggle"), ) } From e80cb942a8bbcc1a72841498c2448d7830af8f85 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 10 Sep 2026 01:24:19 +0530 Subject: [PATCH 17/17] chore: bump version name to 18.0-beta.3 --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9de30b992..113b4e240 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -90,7 +90,7 @@ android { minSdk = 26 targetSdk = 37 versionCode = 62 - versionName = "18.0-beta.2" + versionName = "18.0-beta.3" val whatsNewCounter = 2 buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString())