diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ed3b93dcf1..c2a85aacdd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1738,6 +1738,10 @@
android:name=".receivers.UrlShortenerActionReceiver"
android:exported="false" />
+
+
= Build.VERSION_CODES.R
},
object : Feature(
id = "Lockdown mode",
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
index c6cbab35d1..10d21f9f7d 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/PermissionRegistry.kt
@@ -199,9 +199,10 @@ fun initPermissionRegistry() {
// AOD Wallpaper feature
PermissionRegistry.register("STORAGE", R.string.feat_aod_wallpaper_title)
- // Screenshot share cleanup feature
- PermissionRegistry.register("ACCESSIBILITY", R.string.feat_screenshot_share_cleanup_title)
- PermissionRegistry.register("STORAGE", R.string.feat_screenshot_share_cleanup_title)
+ // Screenshots feature
+ PermissionRegistry.register("ACCESSIBILITY", R.string.feat_screenshots_title)
+ PermissionRegistry.register("STORAGE", R.string.feat_screenshots_title)
+ PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_screenshots_title)
// Pixel Search features
PermissionRegistry.register("STORAGE", R.string.pixel_search_results_files_title)
diff --git a/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt b/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt
new file mode 100644
index 0000000000..249d7976ef
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Background Services & Receivers
+ * File: ScreenshotCleanupActionReceiver.kt
+ * Description: Handles the Keep and Trash now actions on the screenshot cleanup notification.
+ */
+
+package com.sameerasw.essentials.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.ScreenshotCleaner
+import com.sameerasw.essentials.utils.ScreenshotCleanupNotifier
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class ScreenshotCleanupActionReceiver : BroadcastReceiver() {
+ override fun onReceive(
+ context: Context,
+ intent: Intent,
+ ) {
+ val mediaId = intent.getLongExtra(EXTRA_MEDIA_ID, -1L)
+ if (mediaId < 0) return
+ val days = intent.getIntExtra(EXTRA_DAYS, 7)
+ val pending = goAsync()
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ when (intent.action) {
+ ACTION_KEEP -> {
+ ScreenshotCleaner.cancel(context, mediaId)
+ ScreenshotCleanupNotifier.showResult(context, mediaId, R.string.screenshot_kept, days, NOT_TRASHED)
+ }
+
+ ACTION_TRASH_NOW -> {
+ val size = ScreenshotCleaner.trashSized(context, mediaId, keepStarred = false)
+ if (size == null) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, R.string.screenshot_trash_failed, Toast.LENGTH_SHORT).show()
+ }
+ } else {
+ ScreenshotCleaner.cancel(context, mediaId)
+ ScreenshotCleanupNotifier.showResult(context, mediaId, R.string.screenshot_moved_to_trash, days, size)
+ }
+ }
+
+ ACTION_UNDO -> {
+ val trashedBytes = intent.getLongExtra(EXTRA_TRASHED_BYTES, NOT_TRASHED)
+ val restored = trashedBytes == NOT_TRASHED || ScreenshotCleaner.restore(context, mediaId, trashedBytes)
+ if (restored && SettingsRepository(context).isScreenshotAutoCleanEnabled()) {
+ ScreenshotCleaner.schedule(context, mediaId, days * MINUTES_PER_DAY, ScreenshotCleaner.TAG_AUTO_CLEAN)
+ ScreenshotCleanupNotifier.show(context, mediaId, days)
+ } else {
+ ScreenshotCleanupNotifier.dismiss(context, mediaId)
+ }
+ }
+ }
+ } finally {
+ pending.finish()
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_KEEP = "com.sameerasw.essentials.action.KEEP_SCREENSHOT"
+ const val ACTION_TRASH_NOW = "com.sameerasw.essentials.action.TRASH_SCREENSHOT"
+ const val ACTION_UNDO = "com.sameerasw.essentials.action.UNDO_SCREENSHOT_ACTION"
+ const val EXTRA_MEDIA_ID = "extra_media_id"
+ const val EXTRA_DAYS = "extra_days"
+ const val EXTRA_TRASHED_BYTES = "extra_trashed_bytes"
+ private const val NOT_TRASHED = -1L
+ private const val MINUTES_PER_DAY = 24 * 60L
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt b/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt
new file mode 100644
index 0000000000..bb7a0f6bfe
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Background Services & Receivers
+ * File: ScreenshotTrashWorker.kt
+ * Description: Moves a scheduled screenshot to the trash when its cleanup delay runs out.
+ */
+
+package com.sameerasw.essentials.services
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.sameerasw.essentials.utils.ScreenshotCleaner
+import com.sameerasw.essentials.utils.ScreenshotCleanupNotifier
+
+class ScreenshotTrashWorker(
+ appContext: Context,
+ workerParams: WorkerParameters,
+) : CoroutineWorker(appContext, workerParams) {
+ override suspend fun doWork(): Result {
+ val mediaId = inputData.getLong(KEY_MEDIA_ID, -1L)
+ if (mediaId < 0) return Result.failure()
+ ScreenshotCleaner.trash(applicationContext, mediaId, inputData.getBoolean(KEY_KEEP_STARRED, true))
+ ScreenshotCleanupNotifier.dismiss(applicationContext, mediaId)
+ return Result.success()
+ }
+
+ companion object {
+ const val KEY_MEDIA_ID = "media_id"
+ const val KEY_KEEP_STARRED = "keep_starred"
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt
new file mode 100644
index 0000000000..73b889a1a6
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Background Services & Receivers
+ * File: ScreenshotAutoCleanHandler.kt
+ * Description: Schedules every new screenshot for the trash and posts a notification to keep it.
+ */
+
+package com.sameerasw.essentials.services.handlers
+
+import android.content.Context
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.provider.MediaStore
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.ScreenshotCleaner
+import com.sameerasw.essentials.utils.ScreenshotCleanupNotifier
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+class ScreenshotAutoCleanHandler(
+ private val context: Context,
+ private val scope: CoroutineScope,
+) {
+ private val settings = SettingsRepository(context)
+ private val mutex = Mutex()
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val scan = Runnable { scope.launch(Dispatchers.IO) { scheduleNewScreenshots() } }
+
+ private val observer =
+ object : ContentObserver(handler) {
+ override fun onChange(selfChange: Boolean) {
+ handler.removeCallbacks(scan)
+ handler.postDelayed(scan, SCAN_DEBOUNCE_MS)
+ }
+ }
+
+ fun register() {
+ handler.post(scan)
+ context.contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer)
+ }
+
+ fun unregister() {
+ handler.removeCallbacks(scan)
+ context.contentResolver.unregisterContentObserver(observer)
+ }
+
+ private suspend fun scheduleNewScreenshots() {
+ if (!settings.isScreenshotAutoCleanEnabled()) return
+ mutex.withLock {
+ val days = settings.getScreenshotAutoCleanDays()
+ val since = System.currentTimeMillis() / 1000 - NEW_SCREENSHOT_WINDOW_SECONDS
+ ScreenshotCleaner.screenshotIdsAddedSince(context, settings.getScreenshotAutoCleanLastId(), since).forEach { id ->
+ ScreenshotCleaner.schedule(context, id, days * MINUTES_PER_DAY, ScreenshotCleaner.TAG_AUTO_CLEAN)
+ ScreenshotCleanupNotifier.show(context, id, days)
+ settings.setScreenshotAutoCleanLastId(id)
+ }
+ }
+ }
+
+ companion object {
+ private const val MINUTES_PER_DAY = 24 * 60L
+ private const val NEW_SCREENSHOT_WINDOW_SECONDS = 6 * 60 * 60L
+ private const val SCAN_DEBOUNCE_MS = 1_000L
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotShareCleanupHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotShareCleanupHandler.kt
index e48aaccb9b..9107cbe3f8 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotShareCleanupHandler.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotShareCleanupHandler.kt
@@ -4,23 +4,20 @@
*
* Feature Module: Background Services & Receivers
* File: ScreenshotShareCleanupHandler.kt
- * Description: Moves a screenshot to the trash after it is shared from the screenshot preview.
+ * Description: Moves a screenshot to the trash after it is shared from the screenshot preview, right away or after a delay.
*/
package com.sameerasw.essentials.services.handlers
import android.accessibilityservice.AccessibilityService
import android.content.ComponentName
-import android.content.ContentUris
-import android.content.ContentValues
import android.content.res.Resources
-import android.net.Uri
-import android.os.Environment
-import android.provider.MediaStore
import android.view.accessibility.AccessibilityEvent
import android.widget.Toast
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.utils.ScreenshotCleaner
+import com.sameerasw.essentials.utils.ScreenshotCleanupNotifier
import com.sameerasw.essentials.utils.ScreenshotShareTracker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -34,7 +31,7 @@ class ScreenshotShareCleanupHandler(
) {
private val settings = SettingsRepository(service)
private val tracker by lazy { ScreenshotShareTracker(SHARE_SHEET, systemImageEditorPackage() ?: MARKUP) }
- private var screenshotUri: Uri? = null
+ private var screenshotId: Long? = null
private val shareDescription by lazy { systemUiString("screenshot_share_description") ?: "Share screenshot" }
@@ -46,8 +43,8 @@ class ScreenshotShareCleanupHandler(
if (event.packageName == SYSTEM_UI && event.contentDescription?.startsWith(shareDescription) == true) {
val now = System.currentTimeMillis()
tracker.onShareTapped(now, foregroundPackage)
- screenshotUri = null
- scope.launch(Dispatchers.IO) { screenshotUri = findScreenshot(now) }
+ screenshotId = null
+ scope.launch(Dispatchers.IO) { screenshotId = findScreenshot(now) }
}
}
@@ -55,53 +52,41 @@ class ScreenshotShareCleanupHandler(
if (packageName == null) return
val sharedAt = tracker.sharedAt
if (tracker.onForegroundPackage(packageName, System.currentTimeMillis())) {
- trashScreenshot(screenshotUri, sharedAt)
+ trashScreenshot(screenshotId, sharedAt)
}
}
fun onScreenOff() = tracker.onScreenOff()
private fun trashScreenshot(
- capturedUri: Uri?,
+ capturedId: Long?,
sharedAt: Long,
) {
scope.launch {
- delay(TRASH_DELAY_MS)
- val trashed =
+ val delayMinutes = settings.getScreenshotShareCleanupDelayMinutes()
+ if (delayMinutes <= 0) delay(TRASH_DELAY_MS)
+ val outcome =
withContext(Dispatchers.IO) {
- val uri = capturedUri ?: findScreenshot(sharedAt) ?: return@withContext false
- runCatching {
- val values = ContentValues().apply { put(MediaStore.MediaColumns.IS_TRASHED, 1) }
- service.contentResolver.update(uri, values, null) > 0
- }.getOrDefault(false)
+ val mediaId = capturedId ?: findScreenshot(sharedAt) ?: return@withContext null
+ ScreenshotCleanupNotifier.dismiss(service, mediaId)
+ if (delayMinutes > 0) {
+ ScreenshotCleaner.schedule(service, mediaId, delayMinutes.toLong(), ScreenshotCleaner.TAG_SHARED)
+ return@withContext R.string.screenshot_share_cleanup_scheduled
+ }
+ ScreenshotCleaner.cancel(service, mediaId)
+ if (ScreenshotCleaner.trash(service, mediaId, keepStarred = false)) R.string.screenshot_share_cleanup_trashed else null
}
- if (trashed) {
- Toast.makeText(service, R.string.screenshot_share_cleanup_trashed, Toast.LENGTH_SHORT).show()
- }
+ outcome?.let { Toast.makeText(service, it, Toast.LENGTH_SHORT).show() }
}
}
- private fun findScreenshot(sharedAt: Long): Uri? {
- val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- val selection =
- "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATE_ADDED} BETWEEN ? AND ?"
+ private fun findScreenshot(sharedAt: Long): Long? {
val sharedAtSeconds = sharedAt / 1000
- val args =
- arrayOf(
- SCREENSHOTS_PATH,
- (sharedAtSeconds - SCREENSHOT_LOOKBACK_SECONDS).toString(),
- (sharedAtSeconds + SCREENSHOT_LOOKAHEAD_SECONDS).toString(),
- )
- return service.contentResolver
- .query(
- collection,
- arrayOf(MediaStore.MediaColumns._ID),
- selection,
- args,
- "${MediaStore.MediaColumns.DATE_ADDED} DESC",
- )?.use { cursor ->
- if (cursor.moveToFirst()) ContentUris.withAppendedId(collection, cursor.getLong(0)) else null
- }
+ return ScreenshotCleaner.findScreenshotAddedBetween(
+ service,
+ sharedAtSeconds - SCREENSHOT_LOOKBACK_SECONDS,
+ sharedAtSeconds + SCREENSHOT_LOOKAHEAD_SECONDS,
+ )
}
private fun systemUiString(name: String): String? =
@@ -122,7 +107,6 @@ class ScreenshotShareCleanupHandler(
private const val SYSTEM_UI = "com.android.systemui"
private const val SHARE_SHEET = "com.android.intentresolver"
private const val MARKUP = "com.google.android.markup"
- private val SCREENSHOTS_PATH = "${Environment.DIRECTORY_PICTURES}/${Environment.DIRECTORY_SCREENSHOTS}/"
private const val SCREENSHOT_LOOKBACK_SECONDS = 120L
private const val SCREENSHOT_LOOKAHEAD_SECONDS = 5L
private const val TRASH_DELAY_MS = 60_000L
diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
index e7b75d4860..2f0ef76e36 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt
@@ -46,6 +46,7 @@ import com.sameerasw.essentials.services.handlers.NotificationLightingHandler
import com.sameerasw.essentials.services.handlers.OmniGestureOverlayHandler
import com.sameerasw.essentials.services.handlers.FaceUnlockBrightnessHandler
import com.sameerasw.essentials.services.handlers.PocketModeHandler
+import com.sameerasw.essentials.services.handlers.ScreenshotAutoCleanHandler
import com.sameerasw.essentials.services.handlers.ScreenshotShareCleanupHandler
import com.sameerasw.essentials.services.handlers.AppVolumeOverlayHandler
import com.sameerasw.essentials.services.handlers.StatusBarIconHandler
@@ -90,6 +91,7 @@ class ScreenOffAccessibilityService :
lateinit var islandOverlayHandler: IslandCoordinator
private lateinit var statusGlanceHandler: StatusGlanceHandler
private lateinit var screenshotShareCleanupHandler: ScreenshotShareCleanupHandler
+ private lateinit var screenshotAutoCleanHandler: ScreenshotAutoCleanHandler
private lateinit var lockscreenShortcutsHandler: LockscreenShortcutsHandler
private lateinit var appVolumeOverlayHandler: AppVolumeOverlayHandler
@@ -343,9 +345,11 @@ class ScreenOffAccessibilityService :
lockscreenShortcutsHandler = LockscreenShortcutsHandler(this, flashlightHandler)
appVolumeOverlayHandler = AppVolumeOverlayHandler(this)
screenshotShareCleanupHandler = ScreenshotShareCleanupHandler(this, serviceScope)
+ screenshotAutoCleanHandler = ScreenshotAutoCleanHandler(this, serviceScope)
flashlightHandler.register()
statusBarIconHandler.register()
+ screenshotAutoCleanHandler.register()
smartPixelsHandler.init()
duoOverlayHandler.init()
statusGlanceHandler.init()
@@ -562,6 +566,7 @@ class ScreenOffAccessibilityService :
lockscreenShortcutsHandler.onDestroy()
appVolumeOverlayHandler.onDestroy()
statusBarIconHandler.unregister()
+ screenshotAutoCleanHandler.unregister()
stopInputEventListener()
cancelPocketFlashlightTurnOff()
if (isProximityRegistered) {
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 9df16ed07c..062292c610 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
@@ -67,6 +67,7 @@ import com.sameerasw.essentials.ui.core.sheets.PermissionsBottomSheet
import com.sameerasw.essentials.ui.features.battery.BatteriesSettingsUI
import com.sameerasw.essentials.ui.features.consciousgate.CONSCIOUS_GATE_FEATURE_ID
import com.sameerasw.essentials.ui.features.security.AppLockSettingsUI
+import com.sameerasw.essentials.ui.features.security.ScreenshotsSettingsUI
import com.sameerasw.essentials.ui.features.system.AlwaysOnDisplaySettingsUI
import com.sameerasw.essentials.ui.features.system.BACK_GESTURE_FEATURE_ID
import com.sameerasw.essentials.ui.features.system.BackGestureSettingsUI
@@ -381,6 +382,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Notification Sync" -> !isNotificationListenerEnabled
"Button remap" -> !isAccessibilityEnabled
"Face unlock brightness" -> !isAccessibilityEnabled
+ "Screenshots" -> !isAccessibilityEnabled || !viewModel.isStoragePermissionGranted.value
"Lock screen shortcuts" -> !isAccessibilityEnabled
"Volume steps" -> !isAccessibilityEnabled
"App volume" -> !isAccessibilityEnabled || !isShizukuPermissionGranted
@@ -785,7 +787,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
listOf(
"Lockdown mode",
"Face unlock brightness",
- "Screenshot share cleanup",
+ "Screenshots",
),
)
@@ -840,6 +842,7 @@ class FeatureSettingsActivity : AppCompatActivity() {
"Hilight", "Hilight notifications", "Hilight live progress" -> !isNotificationListenerEnabled || !isShizukuPermissionGranted
"Button remap" -> !isAccessibilityEnabled
"Face unlock brightness" -> !isAccessibilityEnabled
+ "Screenshots" -> !isAccessibilityEnabled || !viewModel.isStoragePermissionGranted.value
"Lock screen shortcuts" -> !isAccessibilityEnabled
"Volume steps" -> !isAccessibilityEnabled
"App volume" -> !isAccessibilityEnabled || !isShizukuPermissionGranted
@@ -1175,6 +1178,14 @@ class FeatureSettingsActivity : AppCompatActivity() {
)
}
+ "Screenshots" -> {
+ ScreenshotsSettingsUI(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 16.dp),
+ highlightSetting = highlightSetting,
+ )
+ }
+
"Pocket mode" -> {
PocketModeSettingsUI(
viewModel = viewModel,
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/security/ScreenshotsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/security/ScreenshotsSettingsUI.kt
new file mode 100644
index 0000000000..3dca043f9a
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/security/ScreenshotsSettingsUI.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: UI Feature - Security
+ * File: ScreenshotsSettingsUI.kt
+ * Description: Settings for cleaning up shared and old screenshots.
+ */
+
+package com.sameerasw.essentials.ui.features.security
+
+import android.text.format.Formatter
+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.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+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.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 com.sameerasw.essentials.R
+import com.sameerasw.essentials.ui.core.cards.IconToggleItem
+import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer
+import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker
+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
+
+private val SHARE_DELAY_MINUTES = listOf(0, 60, 24 * 60)
+private val AUTO_CLEAN_DAYS = listOf(1, 7, 30)
+
+@Composable
+fun ScreenshotsSettingsUI(
+ viewModel: MainViewModel,
+ modifier: Modifier = Modifier,
+ highlightSetting: String? = null,
+) {
+ val context = LocalContext.current
+ val view = LocalView.current
+ var requestingPermissionsFor by remember { mutableStateOf?>(null) }
+
+ fun missingPermissions(needsNotifications: Boolean): List =
+ buildList {
+ if (!viewModel.isAccessibilityEnabled.value) add("ACCESSIBILITY")
+ if (!viewModel.isStoragePermissionGranted.value) add("STORAGE")
+ if (needsNotifications && !viewModel.isPostNotificationsEnabled.value) add("POST_NOTIFICATIONS")
+ }
+
+ requestingPermissionsFor?.let { permKeys ->
+ PermissionsBottomSheet(
+ onDismissRequest = {
+ requestingPermissionsFor = null
+ viewModel.check(context)
+ },
+ featureTitle = stringResource(R.string.feat_screenshots_title),
+ permissions = PermissionUIHelper.getPermissionItems(permKeys, context, viewModel),
+ )
+ }
+
+ Column(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ ) {
+ SectionTitle(R.string.screenshots_section_after_sharing)
+ RoundedCardContainer(
+ spacing = 2.dp,
+ cornerRadius = 24.dp,
+ ) {
+ IconToggleItem(
+ iconRes = R.drawable.rounded_share_24,
+ title = stringResource(R.string.feat_screenshot_share_cleanup_title),
+ description = stringResource(R.string.feat_screenshot_share_cleanup_desc),
+ isChecked = viewModel.isScreenshotShareCleanupEnabled.value,
+ onCheckedChange = { checked ->
+ HapticUtil.performVirtualKeyHaptic(view)
+ val missing = missingPermissions(needsNotifications = false)
+ if (checked && missing.isNotEmpty()) {
+ requestingPermissionsFor = missing
+ } else {
+ viewModel.setScreenshotShareCleanupEnabled(checked)
+ }
+ },
+ modifier = Modifier.highlight(highlightSetting == "screenshot_share_cleanup_enabled"),
+ )
+
+ AnimatedVisibility(
+ visible = viewModel.isScreenshotShareCleanupEnabled.value,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ PickerRow(
+ title = R.string.screenshot_share_cleanup_delay_title,
+ highlighted = highlightSetting == "screenshot_share_cleanup_delay",
+ ) {
+ SegmentedPicker(
+ items = SHARE_DELAY_MINUTES,
+ selectedItem = viewModel.screenshotShareCleanupDelayMinutes.intValue,
+ onItemSelected = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ viewModel.setScreenshotShareCleanupDelayMinutes(it)
+ },
+ labelProvider = { minutes ->
+ when (minutes) {
+ 0 -> context.getString(R.string.screenshot_delay_immediately)
+ 60 -> context.getString(R.string.screenshot_delay_1_hour)
+ else -> context.getString(R.string.screenshot_delay_1_day)
+ }
+ },
+ title = R.string.screenshot_share_cleanup_delay_title,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+
+ SectionTitle(R.string.screenshots_section_auto_clean)
+ RoundedCardContainer(
+ spacing = 2.dp,
+ cornerRadius = 24.dp,
+ ) {
+ IconToggleItem(
+ iconRes = R.drawable.rounded_timer_24,
+ title = stringResource(R.string.screenshot_auto_clean_title),
+ description = stringResource(R.string.screenshot_auto_clean_note),
+ isChecked = viewModel.isScreenshotAutoCleanEnabled.value,
+ onCheckedChange = { checked ->
+ HapticUtil.performVirtualKeyHaptic(view)
+ val missing = missingPermissions(needsNotifications = true)
+ if (checked && missing.isNotEmpty()) {
+ requestingPermissionsFor = missing
+ } else {
+ viewModel.setScreenshotAutoCleanEnabled(checked)
+ }
+ },
+ modifier = Modifier.highlight(highlightSetting == "screenshot_auto_clean_enabled"),
+ )
+
+ AnimatedVisibility(
+ visible = viewModel.isScreenshotAutoCleanEnabled.value,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ PickerRow(
+ title = R.string.screenshot_auto_clean_days_title,
+ highlighted = highlightSetting == "screenshot_auto_clean_days",
+ ) {
+ SegmentedPicker(
+ items = AUTO_CLEAN_DAYS,
+ selectedItem = viewModel.screenshotAutoCleanDays.intValue,
+ onItemSelected = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ viewModel.setScreenshotAutoCleanDays(it)
+ },
+ labelProvider = { days -> context.resources.getQuantityString(R.plurals.screenshot_days, days, days) },
+ title = R.string.screenshot_auto_clean_days_title,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+
+ SectionTitle(R.string.screenshots_section_stats)
+ RoundedCardContainer(
+ spacing = 2.dp,
+ cornerRadius = 24.dp,
+ ) {
+ IconToggleItem(
+ iconRes = R.drawable.rounded_delete_24,
+ title = stringResource(R.string.screenshots_cleaned_count_title),
+ description = viewModel.screenshotsCleanedCount.intValue.toString(),
+ showToggle = false,
+ )
+ IconToggleItem(
+ iconRes = R.drawable.rounded_save_24,
+ title = stringResource(R.string.screenshots_storage_saved_title),
+ description = Formatter.formatShortFileSize(context, viewModel.screenshotsCleanedBytes.longValue),
+ showToggle = false,
+ )
+ }
+ }
+}
+
+@Composable
+private fun SectionTitle(title: Int) {
+ Text(
+ text = stringResource(title),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
+ )
+}
+
+@Composable
+private fun PickerRow(
+ title: Int,
+ highlighted: Boolean,
+ content: @Composable () -> Unit,
+) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall)
+ .padding(top = 12.dp)
+ .highlight(highlighted),
+ ) {
+ Text(
+ text = stringResource(title),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ content()
+ }
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt
new file mode 100644
index 0000000000..a765759c9f
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Utilities - Screenshots
+ * File: ScreenshotCleaner.kt
+ * Description: Finds screenshots in MediaStore, moves them to the trash and schedules delayed cleanups.
+ */
+
+package com.sameerasw.essentials.utils
+
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.workDataOf
+import com.sameerasw.essentials.data.repository.SettingsRepository
+import com.sameerasw.essentials.services.ScreenshotTrashWorker
+import java.util.concurrent.TimeUnit
+
+object ScreenshotCleaner {
+ const val TAG_SHARED = "screenshot_cleanup_shared"
+ const val TAG_AUTO_CLEAN = "screenshot_cleanup_auto"
+
+ private val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ private val SCREENSHOTS_PATH = "${Environment.DIRECTORY_PICTURES}/${Environment.DIRECTORY_SCREENSHOTS}/"
+
+ fun findScreenshotAddedBetween(
+ context: Context,
+ fromSeconds: Long,
+ toSeconds: Long,
+ ): Long? =
+ context.contentResolver
+ .query(
+ collection,
+ arrayOf(MediaStore.MediaColumns._ID),
+ "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATE_ADDED} BETWEEN ? AND ?",
+ arrayOf(SCREENSHOTS_PATH, fromSeconds.toString(), toSeconds.toString()),
+ "${MediaStore.MediaColumns.DATE_ADDED} DESC",
+ )?.use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else null }
+
+ fun screenshotIdsAddedSince(
+ context: Context,
+ lastSeenId: Long,
+ sinceSeconds: Long,
+ ): List =
+ context.contentResolver
+ .query(
+ collection,
+ arrayOf(MediaStore.MediaColumns._ID),
+ "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns._ID} > ? AND ${MediaStore.MediaColumns.DATE_ADDED} >= ?",
+ arrayOf(SCREENSHOTS_PATH, lastSeenId.toString(), sinceSeconds.toString()),
+ "${MediaStore.MediaColumns._ID} ASC",
+ )?.use { cursor ->
+ buildList { while (cursor.moveToNext()) add(cursor.getLong(0)) }
+ } ?: emptyList()
+
+ fun latestScreenshotId(context: Context): Long =
+ context.contentResolver
+ .query(
+ collection,
+ arrayOf(MediaStore.MediaColumns._ID),
+ "${MediaStore.MediaColumns.RELATIVE_PATH} = ?",
+ arrayOf(SCREENSHOTS_PATH),
+ "${MediaStore.MediaColumns._ID} DESC",
+ )?.use { cursor -> if (cursor.moveToFirst()) cursor.getLong(0) else 0L } ?: 0L
+
+ fun trash(
+ context: Context,
+ mediaId: Long,
+ keepStarred: Boolean = true,
+ ): Boolean = trashSized(context, mediaId, keepStarred) != null
+
+ fun trashSized(
+ context: Context,
+ mediaId: Long,
+ keepStarred: Boolean = true,
+ ): Long? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
+ val uri = ContentUris.withAppendedId(collection, mediaId)
+ val size =
+ context.contentResolver
+ .query(uri, arrayOf(MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.IS_FAVORITE), null, null, null)
+ ?.use { cursor -> if (cursor.moveToFirst() && !(keepStarred && cursor.getInt(1) == 1)) cursor.getLong(0) else null }
+ ?: return null
+ val trashed =
+ runCatching {
+ val values = ContentValues().apply { put(MediaStore.MediaColumns.IS_TRASHED, 1) }
+ context.contentResolver.update(uri, values, null) > 0
+ }.getOrDefault(false)
+ if (!trashed) return null
+ SettingsRepository(context).recordScreenshotCleaned(size)
+ return size
+ }
+
+ fun restore(
+ context: Context,
+ mediaId: Long,
+ bytes: Long,
+ ): Boolean {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
+ val uri = ContentUris.withAppendedId(collection, mediaId)
+ val extras = Bundle().apply { putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE) }
+ val restored =
+ runCatching {
+ val values = ContentValues().apply { put(MediaStore.MediaColumns.IS_TRASHED, 0) }
+ context.contentResolver.update(uri, values, extras) > 0
+ }.getOrDefault(false)
+ if (restored && bytes > 0) SettingsRepository(context).recordScreenshotRestored(bytes)
+ return restored
+ }
+
+ fun schedule(
+ context: Context,
+ mediaId: Long,
+ delayMinutes: Long,
+ tag: String,
+ ) {
+ val keepStarred = tag != TAG_SHARED
+ val request =
+ OneTimeWorkRequestBuilder()
+ .setInitialDelay(delayMinutes, TimeUnit.MINUTES)
+ .setInputData(workDataOf(ScreenshotTrashWorker.KEY_MEDIA_ID to mediaId, ScreenshotTrashWorker.KEY_KEEP_STARRED to keepStarred))
+ .addTag(tag)
+ .build()
+ WorkManager.getInstance(context).enqueueUniqueWork(workName(mediaId, tag), ExistingWorkPolicy.REPLACE, request)
+ }
+
+ fun cancel(
+ context: Context,
+ mediaId: Long,
+ ) {
+ val workManager = WorkManager.getInstance(context)
+ workManager.cancelUniqueWork(workName(mediaId, TAG_SHARED))
+ workManager.cancelUniqueWork(workName(mediaId, TAG_AUTO_CLEAN))
+ }
+
+ fun cancelAll(
+ context: Context,
+ tag: String,
+ ) {
+ WorkManager.getInstance(context).cancelAllWorkByTag(tag)
+ }
+
+ private fun workName(
+ mediaId: Long,
+ tag: String,
+ ) = "screenshot_trash_${tag}_$mediaId"
+}
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt
new file mode 100644
index 0000000000..21b54a84f5
--- /dev/null
+++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2026 sameerasw.com
+ * License: MIT License
+ *
+ * Feature Module: Utilities - Screenshots
+ * File: ScreenshotCleanupNotifier.kt
+ * Description: Quiet notification for a newly scheduled screenshot with Keep and Trash now actions.
+ */
+
+package com.sameerasw.essentials.utils
+
+import android.Manifest
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.ContentUris
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Size
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import com.sameerasw.essentials.R
+import com.sameerasw.essentials.receivers.ScreenshotCleanupActionReceiver
+
+object ScreenshotCleanupNotifier {
+ private const val CHANNEL_ID = "screenshot_cleanup"
+ private const val RESULT_CHANNEL_ID = "screenshot_cleanup_result"
+ private const val NOTIFICATION_TAG = "screenshot_cleanup"
+ private const val GROUP_KEY = "com.sameerasw.essentials.SCREENSHOT_CLEANUP"
+ private const val TIMEOUT_MILLIS = 60 * 60 * 1000L
+ private const val RESULT_TIMEOUT_MILLIS = 5_000L
+
+ fun show(
+ context: Context,
+ mediaId: Long,
+ days: Int,
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+ createChannels(context)
+
+ val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaId)
+ val openIntent =
+ Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "image/*")
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ val thumbnail = loadThumbnail(context, mediaId)
+
+ val builder =
+ NotificationCompat
+ .Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.rounded_screenshot_region_24)
+ .setContentTitle(context.resources.getQuantityString(R.plurals.screenshot_auto_clean_notification_title, days, days))
+ .setContentText(context.getString(R.string.screenshot_auto_clean_notification_text))
+ .setContentIntent(
+ PendingIntent.getActivity(context, requestCode(mediaId, 0), openIntent, PendingIntent.FLAG_IMMUTABLE),
+ ).addAction(
+ R.drawable.rounded_bookmark_24,
+ context.getString(R.string.screenshot_action_keep),
+ actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_KEEP, 1, days),
+ ).addAction(
+ R.drawable.rounded_delete_24,
+ context.getString(R.string.screenshot_action_trash_now),
+ actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_TRASH_NOW, 2, days),
+ ).setSilent(true)
+ .setAutoCancel(true)
+ .setGroup(GROUP_KEY)
+ .setTimeoutAfter(TIMEOUT_MILLIS)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ if (thumbnail != null) {
+ builder
+ .setLargeIcon(thumbnail)
+ .setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?))
+ }
+ NotificationManagerCompat.from(context).notify(NOTIFICATION_TAG, mediaId.toInt(), builder.build())
+ }
+
+ fun showResult(
+ context: Context,
+ mediaId: Long,
+ message: Int,
+ days: Int,
+ trashedBytes: Long,
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+ createChannels(context)
+ val undo =
+ actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_UNDO, 3, days) { putExtra(ScreenshotCleanupActionReceiver.EXTRA_TRASHED_BYTES, trashedBytes) }
+ val notification =
+ NotificationCompat
+ .Builder(context, RESULT_CHANNEL_ID)
+ .setSmallIcon(R.drawable.rounded_screenshot_region_24)
+ .setContentTitle(context.getString(message))
+ .addAction(R.drawable.ic_undo, context.getString(R.string.screenshot_action_undo), undo)
+ .setSilent(true)
+ .setOnlyAlertOnce(true)
+ .setAutoCancel(true)
+ .setTimeoutAfter(RESULT_TIMEOUT_MILLIS)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+ NotificationManagerCompat.from(context).notify(NOTIFICATION_TAG, mediaId.toInt(), notification)
+ }
+
+ fun dismiss(
+ context: Context,
+ mediaId: Long,
+ ) {
+ NotificationManagerCompat.from(context).cancel(NOTIFICATION_TAG, mediaId.toInt())
+ }
+
+ private fun createChannels(context: Context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+ val manager = context.getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(
+ NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.screenshot_cleanup_channel_name),
+ NotificationManager.IMPORTANCE_LOW,
+ ).apply { setShowBadge(false) },
+ )
+ manager.createNotificationChannel(
+ NotificationChannel(
+ RESULT_CHANNEL_ID,
+ context.getString(R.string.screenshot_cleanup_result_channel_name),
+ NotificationManager.IMPORTANCE_LOW,
+ ).apply { setShowBadge(false) },
+ )
+ }
+
+ private fun actionIntent(
+ context: Context,
+ mediaId: Long,
+ action: String,
+ slot: Int,
+ days: Int,
+ extras: Intent.() -> Unit = {},
+ ): PendingIntent {
+ val intent =
+ Intent(context, ScreenshotCleanupActionReceiver::class.java).apply {
+ this.action = action
+ putExtra(ScreenshotCleanupActionReceiver.EXTRA_MEDIA_ID, mediaId)
+ putExtra(ScreenshotCleanupActionReceiver.EXTRA_DAYS, days)
+ extras()
+ }
+ return PendingIntent.getBroadcast(
+ context,
+ requestCode(mediaId, slot),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ }
+
+ private fun requestCode(
+ mediaId: Long,
+ slot: Int,
+ ) = (mediaId * 4 + slot).toInt()
+
+ private fun loadThumbnail(
+ context: Context,
+ mediaId: Long,
+ ): Bitmap? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null
+ val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaId)
+ return runCatching { context.contentResolver.loadThumbnail(uri, Size(720, 720), null) }.getOrNull()
+ }
+}
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 70c165ecf7..f4d353b747 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -87,6 +87,7 @@ import com.sameerasw.essentials.utils.DeviceUtils
import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.utils.RefreshRateUtils
import com.sameerasw.essentials.utils.RootUtils
+import com.sameerasw.essentials.utils.ScreenshotCleaner
import com.sameerasw.essentials.utils.PrivilegedMode
import com.sameerasw.essentials.utils.ShellUtils
import com.sameerasw.essentials.utils.ShizukuUtils
@@ -403,6 +404,11 @@ class MainViewModel : ViewModel() {
val isPocketModeEnabled = mutableStateOf(false)
val isFaceUnlockBrightnessEnabled = mutableStateOf(false)
val isScreenshotShareCleanupEnabled = mutableStateOf(false)
+ val screenshotShareCleanupDelayMinutes = mutableIntStateOf(0)
+ val isScreenshotAutoCleanEnabled = mutableStateOf(false)
+ val screenshotAutoCleanDays = mutableIntStateOf(7)
+ val screenshotsCleanedCount = mutableIntStateOf(0)
+ val screenshotsCleanedBytes = mutableLongStateOf(0L)
val faceUnlockMaxBrightness = mutableIntStateOf(100)
val faceUnlockAmbientThreshold = mutableFloatStateOf(10f)
val isFaceUnlockTriggerUnlock = mutableStateOf(true)
@@ -1404,6 +1410,21 @@ class MainViewModel : ViewModel() {
SettingsRepository.KEY_SCREENSHOT_SHARE_CLEANUP_ENABLED ->
isScreenshotShareCleanupEnabled.value = settingsRepository.isScreenshotShareCleanupEnabled()
+ SettingsRepository.KEY_SCREENSHOT_SHARE_CLEANUP_DELAY ->
+ screenshotShareCleanupDelayMinutes.intValue = settingsRepository.getScreenshotShareCleanupDelayMinutes()
+
+ SettingsRepository.KEY_SCREENSHOT_AUTO_CLEAN_ENABLED ->
+ isScreenshotAutoCleanEnabled.value = settingsRepository.isScreenshotAutoCleanEnabled()
+
+ SettingsRepository.KEY_SCREENSHOT_AUTO_CLEAN_DAYS ->
+ screenshotAutoCleanDays.intValue = settingsRepository.getScreenshotAutoCleanDays()
+
+ SettingsRepository.KEY_SCREENSHOTS_CLEANED_COUNT ->
+ screenshotsCleanedCount.intValue = settingsRepository.getScreenshotsCleanedCount()
+
+ SettingsRepository.KEY_SCREENSHOTS_CLEANED_BYTES ->
+ screenshotsCleanedBytes.longValue = settingsRepository.getScreenshotsCleanedBytes()
+
SettingsRepository.KEY_FACE_UNLOCK_MAX_BRIGHTNESS ->
faceUnlockMaxBrightness.intValue = settingsRepository.getFaceUnlockMaxBrightness()
@@ -2848,6 +2869,11 @@ class MainViewModel : ViewModel() {
settingsRepository.getBoolean(SettingsRepository.KEY_POCKET_MODE_ENABLED)
isFaceUnlockBrightnessEnabled.value = settingsRepository.isFaceUnlockBrightnessEnabled()
isScreenshotShareCleanupEnabled.value = settingsRepository.isScreenshotShareCleanupEnabled()
+ screenshotShareCleanupDelayMinutes.intValue = settingsRepository.getScreenshotShareCleanupDelayMinutes()
+ isScreenshotAutoCleanEnabled.value = settingsRepository.isScreenshotAutoCleanEnabled()
+ screenshotAutoCleanDays.intValue = settingsRepository.getScreenshotAutoCleanDays()
+ screenshotsCleanedCount.intValue = settingsRepository.getScreenshotsCleanedCount()
+ screenshotsCleanedBytes.longValue = settingsRepository.getScreenshotsCleanedBytes()
faceUnlockMaxBrightness.intValue = settingsRepository.getFaceUnlockMaxBrightness()
faceUnlockAmbientThreshold.floatValue = settingsRepository.getFaceUnlockAmbientThreshold()
isFaceUnlockTriggerUnlock.value = settingsRepository.isFaceUnlockTriggerUnlockEnabled()
@@ -9655,6 +9681,34 @@ class MainViewModel : ViewModel() {
fun setScreenshotShareCleanupEnabled(enabled: Boolean) {
settingsRepository.setScreenshotShareCleanupEnabled(enabled)
isScreenshotShareCleanupEnabled.value = enabled
+ if (!enabled) appContext?.let { ScreenshotCleaner.cancelAll(it, ScreenshotCleaner.TAG_SHARED) }
+ }
+
+ fun setScreenshotShareCleanupDelayMinutes(minutes: Int) {
+ settingsRepository.setScreenshotShareCleanupDelayMinutes(minutes)
+ screenshotShareCleanupDelayMinutes.intValue = minutes
+ }
+
+ fun setScreenshotAutoCleanEnabled(enabled: Boolean) {
+ isScreenshotAutoCleanEnabled.value = enabled
+ val context = appContext
+ if (context == null) {
+ settingsRepository.setScreenshotAutoCleanEnabled(enabled)
+ return
+ }
+ viewModelScope.launch(Dispatchers.IO) {
+ if (enabled) {
+ settingsRepository.setScreenshotAutoCleanLastId(ScreenshotCleaner.latestScreenshotId(context))
+ } else {
+ ScreenshotCleaner.cancelAll(context, ScreenshotCleaner.TAG_AUTO_CLEAN)
+ }
+ settingsRepository.setScreenshotAutoCleanEnabled(enabled)
+ }
+ }
+
+ fun setScreenshotAutoCleanDays(days: Int) {
+ settingsRepository.setScreenshotAutoCleanDays(days)
+ screenshotAutoCleanDays.intValue = days
}
fun setFaceUnlockAmbientThreshold(value: Float) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9bf0029611..7bc5f12cda 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3373,8 +3373,40 @@
Bump brightness in low light
Delete shared screenshots
Trash screenshots after sharing
- After you share a screenshot from the screenshot preview, it is moved to the trash a minute after you leave the app you shared it to. Trashed screenshots can be restored from Files or Photos for 30 days.
Shared screenshot moved to trash
+ Shared screenshot will be moved to trash later
+ Screenshots
+ Clean up shared and old screenshots
+ Keeps your gallery free of one-off screenshots. Shared screenshots can be trashed right after sharing or a little later, and every new screenshot can be trashed automatically after a few days unless you keep it from its notification or star it in Photos. Trashed screenshots can be restored from Files or Photos for 30 days.
+ After sharing
+ Auto-clean
+ Cleaned so far
+ Trash after sharing
+ Immediately
+ 1 hour
+ 1 day
+ Auto-clean screenshots
+ Trash new screenshots after
+ Each new screenshot gets a quiet notification to keep it. Starred screenshots are always kept.
+ Screenshots cleaned
+ Storage saved
+ Tap Keep or star it in Photos to save it
+ Keep
+ Trash now
+ Screenshot kept
+ Screenshot moved to trash
+ Couldn\'t move the screenshot to trash
+ Undo
+ Screenshot cleanup results
+ Screenshot cleanup
+
+ - %d day
+ - %d days
+
+
+ - Screenshot will be trashed in %d day
+ - Screenshot will be trashed in %d days
+
Illuminate
Ambient threshold
Now: %1$s