From 44caae0260d8188876d5519d1d37d34b03a2410f Mon Sep 17 00:00:00 2001 From: Joseph Kuriackal Date: Thu, 15 Oct 2026 08:52:12 +0530 Subject: [PATCH 1/3] feat: screenshots page with delayed share cleanup, auto-clean and stats Moves "Delete shared screenshots" onto a new Screenshots page under Security and adds: - a delay after sharing (immediately, 1 hour, 1 day) - auto-clean: every new screenshot is trashed after 1, 7 or 30 days, with a quiet notification offering Keep and Trash now; starred screenshots are kept - a count of cleaned screenshots and the storage saved New screenshots are picked up with a MediaStore ContentObserver and each cleanup is a unique WorkManager job keyed by the media id. Co-Authored-By: Claude Opus 5.5 (1M context) --- app/src/main/AndroidManifest.xml | 4 + .../data/repository/SettingsRepository.kt | 21 ++ .../domain/registry/FeatureRegistry.kt | 47 +++- .../domain/registry/PermissionRegistry.kt | 7 +- .../ScreenshotCleanupActionReceiver.kt | 59 +++++ .../services/ScreenshotTrashWorker.kt | 33 +++ .../handlers/ScreenshotAutoCleanHandler.kt | 65 +++++ .../handlers/ScreenshotShareCleanupHandler.kt | 68 ++--- .../tiles/ScreenOffAccessibilityService.kt | 5 + .../ui/activities/FeatureSettingsActivity.kt | 13 +- .../security/ScreenshotsSettingsUI.kt | 232 ++++++++++++++++++ .../essentials/utils/ScreenshotCleaner.kt | 114 +++++++++ .../utils/ScreenshotCleanupNotifier.kt | 135 ++++++++++ .../essentials/viewmodels/MainViewModel.kt | 43 ++++ app/src/main/res/values/strings.xml | 30 +++ 15 files changed, 817 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/security/ScreenshotsSettingsUI.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt 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..33f7fb3d2b --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt @@ -0,0 +1,59 @@ +/* + * 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.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 + ScreenshotCleaner.cancel(context, mediaId) + ScreenshotCleanupNotifier.dismiss(context, mediaId) + + when (intent.action) { + ACTION_KEEP -> Toast.makeText(context, R.string.screenshot_kept, Toast.LENGTH_SHORT).show() + + ACTION_TRASH_NOW -> { + val pending = goAsync() + CoroutineScope(Dispatchers.IO).launch { + try { + if (ScreenshotCleaner.trash(context, mediaId, keepStarred = false)) { + withContext(Dispatchers.Main) { + Toast.makeText(context, R.string.screenshot_moved_to_trash, Toast.LENGTH_SHORT).show() + } + } + } 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 EXTRA_MEDIA_ID = "extra_media_id" + } +} 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..ddfef3d95d --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt @@ -0,0 +1,33 @@ +/* + * 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) + ScreenshotCleanupNotifier.dismiss(applicationContext, mediaId) + return Result.success() + } + + companion object { + const val KEY_MEDIA_ID = "media_id" + } +} 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..e6ac125c64 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt @@ -0,0 +1,65 @@ +/* + * 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 observer = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + scope.launch(Dispatchers.IO) { scheduleNewScreenshots() } + } + } + + fun register() { + context.contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer) + } + + fun unregister() { + 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 = 60L + } +} 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..4616a1a0ca 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)) 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..202a7f0c31 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt @@ -0,0 +1,114 @@ +/* + * 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.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 trash( + context: Context, + mediaId: Long, + keepStarred: Boolean = true, + ): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false + 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 false + val trashed = + runCatching { + val values = ContentValues().apply { put(MediaStore.MediaColumns.IS_TRASHED, 1) } + context.contentResolver.update(uri, values, null) > 0 + }.getOrDefault(false) + if (trashed) SettingsRepository(context).recordScreenshotCleaned(size) + return trashed + } + + fun schedule( + context: Context, + mediaId: Long, + delayMinutes: Long, + tag: String, + ) { + val request = + OneTimeWorkRequestBuilder() + .setInitialDelay(delayMinutes, TimeUnit.MINUTES) + .setInputData(workDataOf(ScreenshotTrashWorker.KEY_MEDIA_ID to mediaId)) + .addTag(tag) + .build() + WorkManager.getInstance(context).enqueueUniqueWork(workName(mediaId), ExistingWorkPolicy.REPLACE, request) + } + + fun cancel( + context: Context, + mediaId: Long, + ) { + WorkManager.getInstance(context).cancelUniqueWork(workName(mediaId)) + } + + fun cancelAll( + context: Context, + tag: String, + ) { + WorkManager.getInstance(context).cancelAllWorkByTag(tag) + } + + private fun workName(mediaId: Long) = "screenshot_trash_$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..502c2a375a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt @@ -0,0 +1,135 @@ +/* + * 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 NOTIFICATION_TAG = "screenshot_cleanup" + private const val GROUP_KEY = "com.sameerasw.essentials.SCREENSHOT_CLEANUP" + private const val TIMEOUT_MILLIS = 60 * 60 * 1000L + + 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 + } + createChannel(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), + ).addAction( + R.drawable.rounded_delete_24, + context.getString(R.string.screenshot_action_trash_now), + actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_TRASH_NOW, 2), + ).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 dismiss( + context: Context, + mediaId: Long, + ) { + NotificationManagerCompat.from(context).cancel(NOTIFICATION_TAG, mediaId.toInt()) + } + + private fun createChannel(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val channel = + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.screenshot_cleanup_channel_name), + NotificationManager.IMPORTANCE_LOW, + ).apply { setShowBadge(false) } + context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + + private fun actionIntent( + context: Context, + mediaId: Long, + action: String, + slot: Int, + ): PendingIntent { + val intent = + Intent(context, ScreenshotCleanupActionReceiver::class.java).apply { + this.action = action + putExtra(ScreenshotCleanupActionReceiver.EXTRA_MEDIA_ID, mediaId) + } + return PendingIntent.getBroadcast( + context, + requestCode(mediaId, slot), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun requestCode( + mediaId: Long, + slot: Int, + ) = (mediaId * 3 + 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 ce41913a13..369c783970 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 @@ -398,6 +399,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) @@ -1387,6 +1393,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() @@ -2826,6 +2847,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() @@ -9608,6 +9634,23 @@ 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) { + settingsRepository.setScreenshotAutoCleanEnabled(enabled) + isScreenshotAutoCleanEnabled.value = enabled + if (!enabled) appContext?.let { ScreenshotCleaner.cancelAll(it, ScreenshotCleaner.TAG_AUTO_CLEAN) } + } + + 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 4c5138d153..78c3ca645a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3362,6 +3362,36 @@ 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 + 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 From 06206a67e0c812110a5cbacfbc39f3ab53b610f6 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Fri, 16 Oct 2026 18:46:17 +0530 Subject: [PATCH 2/3] feat: track seen notifications to prioritize unseen alerts during rotation --- .../notifications/NotificationsPlugin.kt | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt index 006670be75..cc10d9ae43 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt @@ -56,6 +56,7 @@ class NotificationsPlugin : BaseIslandPlugin() { ) private val alerts = ArrayDeque() + private val seen = HashSet() private var currentIndex = 0 private var manualPick = false @@ -100,9 +101,28 @@ class NotificationsPlugin : BaseIslandPlugin() { return alerts.drop(index + 1) + alerts.take(index).asReversed() } + private fun markSeen() { + currentAlert()?.let { seen.add(it.key) } + } + private fun advanceCurrent() { if (alerts.size < 2) return currentIndex = (currentIndex.coerceIn(0, alerts.size - 1) + 1) % alerts.size + markSeen() + } + + private fun advanceToUnseen(): Boolean { + if (alerts.size < 2) return false + val start = currentIndex.coerceIn(0, alerts.size - 1) + for (offset in 1 until alerts.size) { + val index = (start + offset) % alerts.size + if (alerts[index].key !in seen) { + currentIndex = index + markSeen() + return true + } + } + return false } private fun select(key: String) { @@ -113,6 +133,7 @@ class NotificationsPlugin : BaseIslandPlugin() { val here = showingHere() if (here && index == currentIndex) return currentIndex = index + markSeen() manualPick = true pendingPopUp = false scheduleTimeout() @@ -152,6 +173,7 @@ class NotificationsPlugin : BaseIslandPlugin() { } cancelTimers() alerts.clear() + seen.clear() } override fun refresh() { @@ -233,6 +255,7 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun popUp() { val c = ctx ?: return if (minimal()) return + markSeen() val lineAvailable = settings.isIslandLineStageEnabled() && settings.isIslandNotifCompactHeadsUpEnabled() c.request( if (lineAvailable) PluginRequest.Peek(ITEM_KEY, settings.getIslandTimeoutMs(), sticky = true) @@ -243,7 +266,7 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun onRemoved(key: String) { val index = alerts.indexOfFirst { it.key == key } if (index < 0) return - alerts.removeAt(index) + alerts.removeAt(index).also { seen.remove(it.key) } if (index < currentIndex) currentIndex-- currentIndex = currentIndex.coerceIn(0, (alerts.size - 1).coerceAtLeast(0)) if (alerts.isEmpty()) clearAll() else render() @@ -252,8 +275,7 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun onTimeout() { val c = ctx ?: return if (pendingPopUp) return - if (showingHere() && alerts.size > 1 && !manualPick) { - advanceCurrent() + if (showingHere() && alerts.size > 1 && !manualPick && advanceToUnseen()) { scheduleTimeout() render() if (c.currentStage() == IslandStage.Line) popUp() @@ -309,6 +331,7 @@ class NotificationsPlugin : BaseIslandPlugin() { catchingUp = false cancelTimers() alerts.clear() + seen.clear() currentIndex = 0 manualPick = false pendingPopUp = false @@ -394,6 +417,7 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun clearWaiting() { val current = currentAlert() ?: return alerts.removeAll { it !== current } + seen.retainAll { it == current.key } currentIndex = 0 render() } From c5883e91935f4e8583727b077bedf282d14e544a Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sun, 18 Oct 2026 08:54:23 +0530 Subject: [PATCH 3/3] feat: add screenshot cleanup undo support, debounced scanning, and improved trash handling --- .../data/repository/SettingsRepository.kt | 15 +++-- .../ScreenshotCleanupActionReceiver.kt | 48 +++++++++++---- .../services/ScreenshotTrashWorker.kt | 3 +- .../handlers/ScreenshotAutoCleanHandler.kt | 14 ++++- .../handlers/ScreenshotShareCleanupHandler.kt | 2 +- .../essentials/utils/ScreenshotCleaner.kt | 59 +++++++++++++++--- .../utils/ScreenshotCleanupNotifier.kt | 60 ++++++++++++++++--- .../essentials/viewmodels/MainViewModel.kt | 15 ++++- app/src/main/res/values/strings.xml | 4 +- 9 files changed, 178 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 13a9d6bb21..73f75474f4 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -4338,10 +4338,17 @@ class SettingsRepository( fun getScreenshotsCleanedCount(): Int = getInt(KEY_SCREENSHOTS_CLEANED_COUNT, 0) fun getScreenshotsCleanedBytes(): Long = getLong(KEY_SCREENSHOTS_CLEANED_BYTES, 0L) - fun recordScreenshotCleaned(bytes: Long) { - putInt(KEY_SCREENSHOTS_CLEANED_COUNT, getScreenshotsCleanedCount() + 1) - putLong(KEY_SCREENSHOTS_CLEANED_BYTES, getScreenshotsCleanedBytes() + bytes) - } + fun recordScreenshotCleaned(bytes: Long) = + synchronized(SettingsRepository::class.java) { + putInt(KEY_SCREENSHOTS_CLEANED_COUNT, getScreenshotsCleanedCount() + 1) + putLong(KEY_SCREENSHOTS_CLEANED_BYTES, getScreenshotsCleanedBytes() + bytes) + } + + fun recordScreenshotRestored(bytes: Long) = + synchronized(SettingsRepository::class.java) { + putInt(KEY_SCREENSHOTS_CLEANED_COUNT, (getScreenshotsCleanedCount() - 1).coerceAtLeast(0)) + putLong(KEY_SCREENSHOTS_CLEANED_BYTES, (getScreenshotsCleanedBytes() - bytes).coerceAtLeast(0L)) + } fun getFaceUnlockMaxBrightness(): Int = getInt(KEY_FACE_UNLOCK_MAX_BRIGHTNESS, 100) fun setFaceUnlockMaxBrightness(value: Int) = putInt(KEY_FACE_UNLOCK_MAX_BRIGHTNESS, value) diff --git a/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt b/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt index 33f7fb3d2b..249d7976ef 100644 --- a/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt +++ b/app/src/main/java/com/sameerasw/essentials/receivers/ScreenshotCleanupActionReceiver.kt @@ -14,6 +14,7 @@ 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 @@ -28,25 +29,41 @@ class ScreenshotCleanupActionReceiver : BroadcastReceiver() { ) { val mediaId = intent.getLongExtra(EXTRA_MEDIA_ID, -1L) if (mediaId < 0) return - ScreenshotCleaner.cancel(context, mediaId) - ScreenshotCleanupNotifier.dismiss(context, mediaId) - - when (intent.action) { - ACTION_KEEP -> Toast.makeText(context, R.string.screenshot_kept, Toast.LENGTH_SHORT).show() + 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 pending = goAsync() - CoroutineScope(Dispatchers.IO).launch { - try { - if (ScreenshotCleaner.trash(context, mediaId, keepStarred = false)) { + ACTION_TRASH_NOW -> { + val size = ScreenshotCleaner.trashSized(context, mediaId, keepStarred = false) + if (size == null) { withContext(Dispatchers.Main) { - Toast.makeText(context, R.string.screenshot_moved_to_trash, Toast.LENGTH_SHORT).show() + 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() } } + } finally { + pending.finish() } } } @@ -54,6 +71,11 @@ class ScreenshotCleanupActionReceiver : BroadcastReceiver() { 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 index ddfef3d95d..bb7a0f6bfe 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/ScreenshotTrashWorker.kt @@ -22,12 +22,13 @@ class ScreenshotTrashWorker( override suspend fun doWork(): Result { val mediaId = inputData.getLong(KEY_MEDIA_ID, -1L) if (mediaId < 0) return Result.failure() - ScreenshotCleaner.trash(applicationContext, mediaId) + 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 index e6ac125c64..73b889a1a6 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ScreenshotAutoCleanHandler.kt @@ -30,18 +30,25 @@ class ScreenshotAutoCleanHandler( 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(Looper.getMainLooper())) { + object : ContentObserver(handler) { override fun onChange(selfChange: Boolean) { - scope.launch(Dispatchers.IO) { scheduleNewScreenshots() } + 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) } @@ -60,6 +67,7 @@ class ScreenshotAutoCleanHandler( companion object { private const val MINUTES_PER_DAY = 24 * 60L - private const val NEW_SCREENSHOT_WINDOW_SECONDS = 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 4616a1a0ca..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 @@ -74,7 +74,7 @@ class ScreenshotShareCleanupHandler( return@withContext R.string.screenshot_share_cleanup_scheduled } ScreenshotCleaner.cancel(service, mediaId) - if (ScreenshotCleaner.trash(service, mediaId)) R.string.screenshot_share_cleanup_trashed else null + if (ScreenshotCleaner.trash(service, mediaId, keepStarred = false)) R.string.screenshot_share_cleanup_trashed else null } outcome?.let { Toast.makeText(service, it, Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt index 202a7f0c31..a765759c9f 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleaner.kt @@ -13,6 +13,7 @@ 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 @@ -60,25 +61,59 @@ object ScreenshotCleaner { 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 { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false + ): 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 false + ?: 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) SettingsRepository(context).recordScreenshotCleaned(size) - return trashed + 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( @@ -87,20 +122,23 @@ object ScreenshotCleaner { delayMinutes: Long, tag: String, ) { + val keepStarred = tag != TAG_SHARED val request = OneTimeWorkRequestBuilder() .setInitialDelay(delayMinutes, TimeUnit.MINUTES) - .setInputData(workDataOf(ScreenshotTrashWorker.KEY_MEDIA_ID to mediaId)) + .setInputData(workDataOf(ScreenshotTrashWorker.KEY_MEDIA_ID to mediaId, ScreenshotTrashWorker.KEY_KEEP_STARRED to keepStarred)) .addTag(tag) .build() - WorkManager.getInstance(context).enqueueUniqueWork(workName(mediaId), ExistingWorkPolicy.REPLACE, request) + WorkManager.getInstance(context).enqueueUniqueWork(workName(mediaId, tag), ExistingWorkPolicy.REPLACE, request) } fun cancel( context: Context, mediaId: Long, ) { - WorkManager.getInstance(context).cancelUniqueWork(workName(mediaId)) + val workManager = WorkManager.getInstance(context) + workManager.cancelUniqueWork(workName(mediaId, TAG_SHARED)) + workManager.cancelUniqueWork(workName(mediaId, TAG_AUTO_CLEAN)) } fun cancelAll( @@ -110,5 +148,8 @@ object ScreenshotCleaner { WorkManager.getInstance(context).cancelAllWorkByTag(tag) } - private fun workName(mediaId: Long) = "screenshot_trash_$mediaId" + 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 index 502c2a375a..21b54a84f5 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/ScreenshotCleanupNotifier.kt @@ -29,9 +29,11 @@ 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, @@ -43,7 +45,7 @@ object ScreenshotCleanupNotifier { ) { return } - createChannel(context) + createChannels(context) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaId) val openIntent = @@ -64,11 +66,11 @@ object ScreenshotCleanupNotifier { ).addAction( R.drawable.rounded_bookmark_24, context.getString(R.string.screenshot_action_keep), - actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_KEEP, 1), + 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), + actionIntent(context, mediaId, ScreenshotCleanupActionReceiver.ACTION_TRASH_NOW, 2, days), ).setSilent(true) .setAutoCancel(true) .setGroup(GROUP_KEY) @@ -82,6 +84,36 @@ object ScreenshotCleanupNotifier { 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, @@ -89,15 +121,23 @@ object ScreenshotCleanupNotifier { NotificationManagerCompat.from(context).cancel(NOTIFICATION_TAG, mediaId.toInt()) } - private fun createChannel(context: Context) { + private fun createChannels(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - val channel = + 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) } - context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + ).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( @@ -105,11 +145,15 @@ object ScreenshotCleanupNotifier { 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, @@ -122,7 +166,7 @@ object ScreenshotCleanupNotifier { private fun requestCode( mediaId: Long, slot: Int, - ) = (mediaId * 3 + slot).toInt() + ) = (mediaId * 4 + slot).toInt() private fun loadThumbnail( context: Context, 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 46be93d1ba..f4d353b747 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -9690,9 +9690,20 @@ class MainViewModel : ViewModel() { } fun setScreenshotAutoCleanEnabled(enabled: Boolean) { - settingsRepository.setScreenshotAutoCleanEnabled(enabled) isScreenshotAutoCleanEnabled.value = enabled - if (!enabled) appContext?.let { ScreenshotCleaner.cancelAll(it, ScreenshotCleaner.TAG_AUTO_CLEAN) } + 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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7edd6b200a..7bc5f12cda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3373,7 +3373,6 @@ 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 @@ -3396,6 +3395,9 @@ Trash now Screenshot kept Screenshot moved to trash + Couldn\'t move the screenshot to trash + Undo + Screenshot cleanup results Screenshot cleanup %d day