Skip to content

Commit

Permalink
Merge pull request #593 from d4rken-org/systemcleaner_filter_imexport
Browse files Browse the repository at this point in the history
SystemCleaner: Support filter import & export
  • Loading branch information
d4rken authored Aug 18, 2023
2 parents 9934f4d + 2216b9f commit 012a2df
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 4 deletions.
11 changes: 10 additions & 1 deletion app-common/src/main/java/eu/darken/sdmse/common/UriExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package eu.darken.sdmse.common

import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import okio.buffer
import okio.source

fun Uri.dropLastColon(): Uri = Uri.parse(toString().removeSuffix(Uri.encode(":")))
fun Uri.dropLastColon(): Uri = Uri.parse(toString().removeSuffix(Uri.encode(":")))

@SuppressLint("Recycle")
fun Uri.read(context: Context) = context.contentResolver.openInputStream(this)?.source()

fun Uri.readAsText(context: Context) = read(context)?.buffer()?.readUtf8()
6 changes: 6 additions & 0 deletions app/src/main/java/eu/darken/sdmse/common/MimeTypes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package eu.darken.sdmse.common

sealed class MimeTypes(val value: String) {

object Json : MimeTypes("application/json")
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,29 @@ class CustomFilterRepo @Inject constructor(

fun generateIdentifier() = UUID.randomUUID().toString()

suspend fun importFilter(rawFilters: List<RawFilter>) {
log(TAG) { "importFilter($rawFilters)" }
val configs = rawFilters.mapNotNull {
try {
configAdapter.fromJson(it.payload)
} catch (e: Exception) {
log(TAG, ERROR) { "Failed to import $it" }
null
}
}.toSet()
save(configs)
}

suspend fun exportFilters(identifiers: Collection<FilterIdentifier>): Collection<RawFilter> {
log(TAG) { "exportFilters($identifiers)" }
val configs = currentConfigs().filter { identifiers.contains(it.identifier) }

return configs.map {
val rawJson = configAdapter.toJson(it)
RawFilter("${it.label} - ${it.identifier.takeLast(10)}.json", rawJson)
}
}

companion object {
private val TAG = logTag("SystemCleaner", "CustomFilter", "Repo")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package eu.darken.sdmse.systemcleaner.core.filter.custom

data class RawFilter(
val name: String,
val payload: String,
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package eu.darken.sdmse.systemcleaner.ui.customfilter.list

import android.content.Intent
import eu.darken.sdmse.systemcleaner.core.filter.custom.CustomFilterConfig
import eu.darken.sdmse.systemcleaner.core.filter.custom.RawFilter

sealed class CustomFilterListEvents {
data class UndoRemove(val exclusions: Set<CustomFilterConfig>) : CustomFilterListEvents()
data class ImportEvent(val intent: Intent) : CustomFilterListEvents()
data class ExportEvent(val intent: Intent, val filter: Collection<RawFilter>) : CustomFilterListEvents()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package eu.darken.sdmse.systemcleaner.ui.customfilter.list

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
Expand All @@ -13,6 +18,9 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.sdmse.R
import eu.darken.sdmse.common.WebpageTool
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.lists.differ.update
import eu.darken.sdmse.common.lists.installListSelection
import eu.darken.sdmse.common.lists.setupDefaults
Expand All @@ -30,16 +38,61 @@ class CustomFilterListFragment : Fragment3(R.layout.systemcleaner_customfilter_l
override val ui: SystemcleanerCustomfilterListFragmentBinding by viewBinding()
@Inject lateinit var webpageTool: WebpageTool

private lateinit var importPickerLauncher: ActivityResultLauncher<Intent>
private lateinit var exportPickerLauncher: ActivityResultLauncher<Intent>

private var currentSnackbar: Snackbar? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
importPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
log(TAG, WARN) { "importPickerLauncher returned ${result.resultCode}: ${result.data}" }
return@registerForActivityResult
}

val uriList = mutableListOf<Uri>()

val clipData = result.data?.clipData
if (clipData != null) {
(0 until clipData.itemCount).forEach {
uriList.add(clipData.getItemAt(it).uri)
}
} else {
result.data?.data?.let { uriList.add(it) }
}

vm.importFilter(uriList)
}

exportPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
log(TAG, WARN) { "exportPickerLauncher returned ${result.resultCode}: ${result.data}" }
return@registerForActivityResult
}

result.data?.data?.let { uri ->
vm.performExport(uri)
}
}

ui.toolbar.apply {
setupWithNavController(findNavController())
setOnMenuItemClickListener {
when (it.itemId) {
setOnMenuItemClickListener { menuItem ->
currentSnackbar?.let {
it.dismiss()
currentSnackbar = null
}
when (menuItem.itemId) {
R.id.menu_action_help -> {
webpageTool.open("https://github.com/d4rken-org/sdmaid-se/wiki/SystemCleaner#custom-filter")
true
}

R.id.menu_action_import -> {
vm.importFilter()
true
}

else -> false
}
}
Expand All @@ -52,6 +105,10 @@ class CustomFilterListFragment : Fragment3(R.layout.systemcleaner_customfilter_l
cabMenuRes = R.menu.menu_systemcleaner_customfilter_list_cab,
onPrepare = { tracker, _, menu ->
menu.findItem(R.id.action_edit_selected)?.isVisible = tracker.selection.size() == 1
currentSnackbar?.let {
it.dismiss()
currentSnackbar = null
}
true
},
onSelected = { tracker: SelectionTracker<String>, item: MenuItem, selected: List<CustomFilterListAdapter.Item> ->
Expand All @@ -68,6 +125,12 @@ class CustomFilterListFragment : Fragment3(R.layout.systemcleaner_customfilter_l
true
}

R.id.menu_action_export -> {
vm.exportFilter(selected)
tracker.clearSelection()
true
}

else -> false
}
},
Expand Down Expand Up @@ -110,11 +173,23 @@ class CustomFilterListFragment : Fragment3(R.layout.systemcleaner_customfilter_l
.setAction(eu.darken.sdmse.common.R.string.general_undo_action) {
vm.restore(event.exclusions)
}
.also { currentSnackbar = it }
.show()

is CustomFilterListEvents.ImportEvent -> {
importPickerLauncher.launch(event.intent)
}

is CustomFilterListEvents.ExportEvent -> {
exportPickerLauncher.launch(event.intent)
}
}
}

super.onViewCreated(view, savedInstanceState)
}

companion object {
private val TAG = logTag("SystemCleaner", "CustomFilter", "List")
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
package eu.darken.sdmse.systemcleaner.ui.customfilter.list

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.sdmse.common.MimeTypes
import eu.darken.sdmse.common.SingleLiveEvent
import eu.darken.sdmse.common.coroutine.DispatcherProvider
import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN
import eu.darken.sdmse.common.debug.logging.log
import eu.darken.sdmse.common.debug.logging.logTag
import eu.darken.sdmse.common.readAsText
import eu.darken.sdmse.common.uix.ViewModel3
import eu.darken.sdmse.common.upgrade.UpgradeRepo
import eu.darken.sdmse.common.upgrade.isPro
import eu.darken.sdmse.main.ui.dashboard.items.*
import eu.darken.sdmse.systemcleaner.core.SystemCleanerSettings
import eu.darken.sdmse.systemcleaner.core.filter.custom.CustomFilterConfig
import eu.darken.sdmse.systemcleaner.core.filter.custom.CustomFilterRepo
import eu.darken.sdmse.systemcleaner.core.filter.custom.RawFilter
import eu.darken.sdmse.systemcleaner.core.filter.custom.toggleCustomFilter
import eu.darken.sdmse.systemcleaner.ui.customfilter.list.types.CustomFilterDefaultVH
import kotlinx.coroutines.flow.*
import java.io.IOException
import javax.inject.Inject

@HiltViewModel
class CustomFilterListViewModel @Inject constructor(
@Suppress("unused") private val handle: SavedStateHandle,
dispatcherProvider: DispatcherProvider,
@ApplicationContext private val context: Context,
@Suppress("StaticFieldLeak") @ApplicationContext private val context: Context,
private val customFilterRepo: CustomFilterRepo,
private val systemCleanerSettings: SystemCleanerSettings,
private val upgradeRepo: UpgradeRepo,
Expand Down Expand Up @@ -83,11 +92,81 @@ class CustomFilterListViewModel @Inject constructor(

fun edit(item: CustomFilterListAdapter.Item) = launch {
log(TAG) { "edit($item)" }

if (!upgradeRepo.isPro()) {
log(TAG) { "Pro upgrade required" }
CustomFilterListFragmentDirections.goToUpgradeFragment().navigate()
return@launch
}

CustomFilterListFragmentDirections.actionCustomFilterListFragmentToCustomFilterEditorFragment(
identifier = item.config.identifier
).navigate()
}

fun importFilter(uris: Collection<Uri>? = null) = launch {
log(TAG) { "importFilter($uris)" }
if (uris == null) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = MimeTypes.Json.value
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
events.postValue(CustomFilterListEvents.ImportEvent(intent))
return@launch
}

val rawFilter = uris
.mapNotNull {
val result = it.readAsText(context) ?: throw IllegalArgumentException("Failed to read $it")
log(TAG) { "Read $it: $result" }
it to result
}
.map { RawFilter(it.first.toString(), it.second) }

customFilterRepo.importFilter(rawFilter)
}

private var stagedExport: Collection<RawFilter>? = null
fun exportFilter(items: Collection<CustomFilterListAdapter.Item>) = launch {
log(TAG) { "exportFilter($items)" }

if (!upgradeRepo.isPro()) {
log(TAG) { "Pro upgrade required" }
CustomFilterListFragmentDirections.goToUpgradeFragment().navigate()
return@launch
}

val rawFilter = customFilterRepo.exportFilters(items.map { it.config.identifier })
stagedExport = rawFilter

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
events.postValue(CustomFilterListEvents.ExportEvent(intent, rawFilter))
}

fun performExport(directoryUri: Uri?) = launch {
if (directoryUri == null) {
log(TAG, WARN) { "Export failed, no path picked" }
return@launch
}

val exportData = stagedExport ?: throw IllegalStateException("No staged export data available")

val saveDir = DocumentFile.fromTreeUri(context, directoryUri)
?: throw IOException("Failed to access $directoryUri")

exportData.forEach { rawFilter ->
val targetFile = saveDir.createFile(MimeTypes.Json.value, rawFilter.name)
?: throw IOException("Failed to create ${rawFilter.name} in $saveDir")

context.contentResolver.openOutputStream(targetFile.uri)?.use { out ->
out.write(rawFilter.payload.toByteArray())
}

log(TAG) { "Wrote ${rawFilter.name} to $targetFile" }
}
}

companion object {
private val TAG = logTag("SystemCleaner", "CustomFilter", "List", "ViewModel")
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_file_export_outline_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M18 20H6V4H13V9H18V20M16 11V18.1L13.9 16L11.1 18.8L8.3 16L11.1 13.2L8.9 11H16Z" />
</vector>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_file_import_outline_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14 2H6C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2M18 20H6V4H13V9H18V20M15 11.93V19H7.93L10.05 16.88L7.22 14.05L10.05 11.22L12.88 14.05L15 11.93Z" />
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.darken.sdmse.main.ui.MainActivity">

<item
android:id="@+id/menu_action_import"
android:icon="@drawable/ic_file_import_outline_24"
android:orderInCategory="100"
android:title="@string/systemcleaner_customfilter_import_action"
app:showAsAction="ifRoom" />

<item
android:id="@+id/menu_action_help"
android:icon="@drawable/ic_help_outline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
android:title="@string/general_edit_action"
app:showAsAction="ifRoom" />

<item
android:id="@+id/menu_action_export"
android:icon="@drawable/ic_file_export_outline_24"
android:orderInCategory="100"
android:title="@string/systemcleaner_customfilter_export_action"
app:showAsAction="ifRoom" />

<item
android:id="@+id/action_select_all"
android:icon="@drawable/baseline_select_all_24"
Expand Down
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 @@ -304,6 +304,8 @@
<string name="systemcleaner_customfilter_editor_name_matching_mode_equal_label">Name is equal to keyword</string>
<string name="systemcleaner_customfilter_last_edit">Last edit: %s</string>
<string name="systemcleaner_customfilter_editor_livesearch_label">Live search</string>
<string name="systemcleaner_customfilter_import_action">Import new filter</string>
<string name="systemcleaner_customfilter_export_action">Export selected filter</string>

<string name="appcleaner_tool_name">AppCleaner</string>
<string name="appcleaner_explanation_short">Expendable data belonging to specific apps.</string>
Expand Down

0 comments on commit 012a2df

Please sign in to comment.