From f9b82aedab2dfafbb869be8ba0cfc3a65ce8f217 Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 31 Jan 2025 00:49:07 +0100 Subject: [PATCH] SystemCleaner: New filter for finding trashed files by galleries --- .../core/SystemCleanerSettings.kt | 3 + .../core/filter/stock/TrashedFilter.kt | 89 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + .../res/xml/preferences_systemcleaner.xml | 6 ++ .../filter/stock/ScreenshotsFilterTest.kt | 50 +++++++++++ .../core/filter/stock/TrashedFilterTest.kt | 48 ++++++++++ 6 files changed, 198 insertions(+) create mode 100644 app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilter.kt create mode 100644 app/src/test/java/eu/darken/sdmse/systemcleaner/core/filter/stock/ScreenshotsFilterTest.kt create mode 100644 app/src/test/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilterTest.kt diff --git a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/SystemCleanerSettings.kt b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/SystemCleanerSettings.kt index ae8c466bc..ffd1fb682 100644 --- a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/SystemCleanerSettings.kt +++ b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/SystemCleanerSettings.kt @@ -50,6 +50,8 @@ class SystemCleanerSettings @Inject constructor( val filterScreenshotsEnabled = dataStore.createValue("filter.screenshots.enabled", false) val filterScreenshotsAge = dataStore.createValue("filter.screenshots.age", SCREENSHOTS_AGE_DEFAULT, moshi) + val filterTrashedEnabled = dataStore.createValue("filter.trashed.enabled", false) + val enabledCustomFilter = dataStore.createValue( "filter.custom.enabled", emptySet(), @@ -63,6 +65,7 @@ class SystemCleanerSettings @Inject constructor( filterSuperfluosApksEnabled, filterScreenshotsEnabled, filterScreenshotsAge, + filterTrashedEnabled, filterLostDirEnabled, filterLinuxFilesEnabled, filterMacFilesEnabled, diff --git a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilter.kt b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilter.kt new file mode 100644 index 000000000..515c344b1 --- /dev/null +++ b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilter.kt @@ -0,0 +1,89 @@ +package eu.darken.sdmse.systemcleaner.core.filter.stock + +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import eu.darken.sdmse.R +import eu.darken.sdmse.common.areas.DataArea +import eu.darken.sdmse.common.ca.CaDrawable +import eu.darken.sdmse.common.ca.CaString +import eu.darken.sdmse.common.ca.toCaDrawable +import eu.darken.sdmse.common.ca.toCaString +import eu.darken.sdmse.common.datastore.value +import eu.darken.sdmse.common.debug.logging.log +import eu.darken.sdmse.common.debug.logging.logTag +import eu.darken.sdmse.common.files.APathLookup +import eu.darken.sdmse.common.files.GatewaySwitch +import eu.darken.sdmse.systemcleaner.core.SystemCleanerSettings +import eu.darken.sdmse.systemcleaner.core.filter.BaseSystemCleanerFilter +import eu.darken.sdmse.systemcleaner.core.filter.SystemCleanerFilter +import eu.darken.sdmse.systemcleaner.core.filter.toDeletion +import eu.darken.sdmse.systemcleaner.core.sieve.BaseSieve +import eu.darken.sdmse.systemcleaner.core.sieve.NameCriterium +import javax.inject.Inject +import javax.inject.Provider + +class TrashedFilter @Inject constructor( + private val baseSieveFactory: BaseSieve.Factory, + private val gatewaySwitch: GatewaySwitch, +) : BaseSystemCleanerFilter() { + + override suspend fun getIcon(): CaDrawable = R.drawable.ic_recycle_bin_24.toCaDrawable() + + override suspend fun getLabel(): CaString = R.string.systemcleaner_filter_trashed_label.toCaString() + + override suspend fun getDescription(): CaString { + return R.string.systemcleaner_filter_trashed_summary.toCaString() + } + + override suspend fun targetAreas(): Set = setOf( + DataArea.Type.SDCARD, + DataArea.Type.PORTABLE, + ) + + private lateinit var sieve: BaseSieve + + override suspend fun initialize() { + val config = BaseSieve.Config( + targetTypes = setOf(BaseSieve.TargetType.FILE), + areaTypes = targetAreas(), + nameCriteria = setOf( + NameCriterium(".trashed-", mode = NameCriterium.Mode.Start()), + ), + ) + sieve = baseSieveFactory.create(config) + log(TAG) { "initialized() with $config" } + } + + override suspend fun match(item: APathLookup<*>): SystemCleanerFilter.Match? { + return sieve.match(item).toDeletion() + } + + override suspend fun process(matches: Collection) { + matches.deleteAll(gatewaySwitch) + } + + override fun toString(): String = "${this::class.simpleName}(${hashCode()})" + + @Reusable + class Factory @Inject constructor( + private val settings: SystemCleanerSettings, + private val filterProvider: Provider + ) : SystemCleanerFilter.Factory { + override suspend fun isEnabled(): Boolean = settings.filterTrashedEnabled.value() + override suspend fun create(): SystemCleanerFilter = filterProvider.get() + } + + @InstallIn(SingletonComponent::class) + @Module + abstract class DIM { + @Binds @IntoSet abstract fun mod(mod: Factory): SystemCleanerFilter.Factory + } + + companion object { + private val TAG = logTag("SystemCleaner", "Filter", "Trashed") + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d09c12d7..74bbf40f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -273,6 +273,8 @@ Empty folders from all over the device. Nested folders may require multiple passes. Screenshots Screenshots older than %s from multiple apps and locations. + Trashed files + Files in recycle bins from various apps and locations that are marked for deletion but not yet permanently removed. Screenshot Age Minimum age for screenshots to be considered for deletion. Superfluous APKs diff --git a/app/src/main/res/xml/preferences_systemcleaner.xml b/app/src/main/res/xml/preferences_systemcleaner.xml index f2d7f0598..525721b98 100644 --- a/app/src/main/res/xml/preferences_systemcleaner.xml +++ b/app/src/main/res/xml/preferences_systemcleaner.xml @@ -35,6 +35,12 @@ app:singleLineTitle="false" app:summary="@string/systemcleaner_filter_superfluosapks_summary" app:title="@string/systemcleaner_filter_superfluosapks_label" /> + ().apply { + every { filterScreenshotsAge } returns mockk>().apply { + every { flow } returns flowOf(Duration.ofDays(11)) + } + } + + @BeforeEach + override fun setup() { + super.setup() + } + + @AfterEach + override fun teardown() { + super.teardown() + } + + private fun create() = ScreenshotsFilter( + baseSieveFactory = object : BaseSieve.Factory { + override fun create(config: BaseSieve.Config): BaseSieve = BaseSieve(config, fileForensics) + }, + gatewaySwitch = gatewaySwitch, + settings = settings, + ) + + @Test fun testFilter() = runTest { + mockDefaults() + neg(SDCARD, "Pictures/Screenshots", Flag.Dir) + pos(SDCARD, "Pictures/Screenshots/123ABC.png", Flag.File) + pos(SDCARD, "Pictures/Screenshots/456DEF.jpg", Flag.File) + confirm(create()) + } +} diff --git a/app/src/test/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilterTest.kt b/app/src/test/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilterTest.kt new file mode 100644 index 000000000..b5e217f22 --- /dev/null +++ b/app/src/test/java/eu/darken/sdmse/systemcleaner/core/filter/stock/TrashedFilterTest.kt @@ -0,0 +1,48 @@ +package eu.darken.sdmse.systemcleaner.core.filter.stock + +import eu.darken.sdmse.common.areas.DataArea.Type.PORTABLE +import eu.darken.sdmse.common.areas.DataArea.Type.SDCARD +import eu.darken.sdmse.systemcleaner.core.filter.SystemCleanerFilterTest +import eu.darken.sdmse.systemcleaner.core.sieve.BaseSieve +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class TrashedFilterTest : SystemCleanerFilterTest() { + + @BeforeEach + override fun setup() { + super.setup() + } + + @AfterEach + override fun teardown() { + super.teardown() + } + + private fun create() = TrashedFilter( + baseSieveFactory = object : BaseSieve.Factory { + override fun create(config: BaseSieve.Config): BaseSieve = BaseSieve(config, fileForensics) + }, + gatewaySwitch = gatewaySwitch, + ) + + @Test fun testFilter() = runTest { + mockDefaults() + + neg(SDCARD, "Pictures/1740850032-PXL_20250130_172627042.jpg", Flag.File) + pos(SDCARD, "Pictures/.trashed-1740850032-PXL_20250130_172627042.jpg", Flag.File) + neg(SDCARD, "DCIM/Camera/.trashed-1740849860-PXL_20241015_101215095.TS.mp4", Flag.Dir) + neg(SDCARD, "DCIM/Camera/1740849860-PXL_20241015_101215095.TS.mp4", Flag.File) + pos(SDCARD, "DCIM/Camera/.trashed-1740849860-PXL_20241015_101215095.TS.mp4", Flag.File) + + neg(PORTABLE, "Pictures/1740850032-PXL_20250130_172627042.jpg", Flag.File) + pos(PORTABLE, "Pictures/.trashed-1740850032-PXL_20250130_172627042.jpg", Flag.File) + neg(PORTABLE, "DCIM/Camera/.trashed-1740849860-PXL_20241015_101215095.TS.mp4", Flag.Dir) + neg(PORTABLE, "DCIM/Camera/1740849860-PXL_20241015_101215095.TS.mp4", Flag.File) + pos(PORTABLE, "DCIM/Camera/.trashed-1740849860-PXL_20241015_101215095.TS.mp4", Flag.File) + + confirm(create()) + } +}