Skip to content

Commit

Permalink
SystemCleaner: New filter for finding trashed files by galleries
Browse files Browse the repository at this point in the history
  • Loading branch information
d4rken committed Jan 31, 2025
1 parent 7fd226f commit f9b82ae
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<FilterIdentifier>(),
Expand All @@ -63,6 +65,7 @@ class SystemCleanerSettings @Inject constructor(
filterSuperfluosApksEnabled,
filterScreenshotsEnabled,
filterScreenshotsAge,
filterTrashedEnabled,
filterLostDirEnabled,
filterLinuxFilesEnabled,
filterMacFilesEnabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DataArea.Type> = 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<SystemCleanerFilter.Match>) {
matches.deleteAll(gatewaySwitch)
}

override fun toString(): String = "${this::class.simpleName}(${hashCode()})"

@Reusable
class Factory @Inject constructor(
private val settings: SystemCleanerSettings,
private val filterProvider: Provider<TrashedFilter>
) : 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")
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@
<string name="systemcleaner_filter_emptydirectories_summary">Empty folders from all over the device. Nested folders may require multiple passes.</string>
<string name="systemcleaner_filter_screenshots_label">Screenshots</string>
<string name="systemcleaner_filter_screenshots_summary">Screenshots older than %s from multiple apps and locations.</string>
<string name="systemcleaner_filter_trashed_label">Trashed files</string>
<string name="systemcleaner_filter_trashed_summary">Files in recycle bins from various apps and locations that are marked for deletion but not yet permanently removed.</string>
<string name="systemcleaner_filter_screenshots_age_label">Screenshot Age</string>
<string name="systemcleaner_filter_screenshots_age_summary">Minimum age for screenshots to be considered for deletion.</string>
<string name="systemcleaner_filter_superfluosapks_label">Superfluous APKs</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/preferences_systemcleaner.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
app:singleLineTitle="false"
app:summary="@string/systemcleaner_filter_superfluosapks_summary"
app:title="@string/systemcleaner_filter_superfluosapks_label" />
<CheckBoxPreference
app:icon="@drawable/ic_recycle_bin_24"
app:key="filter.trashed.enabled"
app:singleLineTitle="false"
app:summary="@string/systemcleaner_filter_trashed_summary"
app:title="@string/systemcleaner_filter_trashed_label" />
<CheckBoxPreference
app:icon="@drawable/ic_cellphone_screenshot_24"
app:key="filter.screenshots.enabled"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package eu.darken.sdmse.systemcleaner.core.filter.stock

import eu.darken.sdmse.common.areas.DataArea.Type.SDCARD
import eu.darken.sdmse.common.datastore.DataStoreValue
import eu.darken.sdmse.systemcleaner.core.SystemCleanerSettings
import eu.darken.sdmse.systemcleaner.core.filter.SystemCleanerFilterTest
import eu.darken.sdmse.systemcleaner.core.sieve.BaseSieve
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Duration

class ScreenshotsFilterTest : SystemCleanerFilterTest() {

private val settings = mockk<SystemCleanerSettings>().apply {
every { filterScreenshotsAge } returns mockk<DataStoreValue<Duration>>().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())
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}

0 comments on commit f9b82ae

Please sign in to comment.