Skip to content

Commit

Permalink
feat: save patch options and selected patches in bundle (ReVanced#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 committed Jul 4, 2023
1 parent 01fd4c8 commit 8dd8f88
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.util.PathSaver
import app.revanced.manager.util.saver.PathSaver
import java.nio.file.Path
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SourceRepository
Expand All @@ -19,6 +23,8 @@ import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet
import app.revanced.manager.util.flatMapLatestAndCombine
import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateMapSaver
import app.revanced.manager.util.saver.snapshotStateSetSaver
import app.revanced.manager.util.toMutableStateSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
Expand All @@ -29,11 +35,13 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get

@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(
val appInfo: AppInfo
) : ViewModel(), KoinComponent {
private val selectionRepository: PatchSelectionRepository = get()
private val prefs: PreferencesManager = get()
private val savedStateHandle: SavedStateHandle = get()
val allowExperimental get() = prefs.allowExperimental

val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
Expand All @@ -59,9 +67,30 @@ class PatchesSelectorViewModel(
}
}

private val selectedPatches = mutableStateMapOf<Int, SnapshotStateSet<String>>()
private val patchOptions =
mutableStateMapOf<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>()
private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = {
val map: SnapshotStatePatchesSelection = mutableStateMapOf()
viewModelScope.launch(Dispatchers.Default) {
val bundles = bundlesFlow.first()
val filteredSelection =
selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) ->
// Filter out patches that don't exist.
val filteredPatches = bundles.singleOrNull { it.uid == uid }
?.let { bundle ->
val allPatches = bundle.all.map { it.name }
patches.filter { allPatches.contains(it) }
}
?: patches

filteredPatches.toMutableStateSet()
}

withContext(Dispatchers.Main) {
map.putAll(filteredSelection)
}
}
return@saveable map
})
private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(saver = optionsSaver, init = ::mutableStateMapOf)

/**
* Show the patch options dialog for this patch.
Expand Down Expand Up @@ -91,38 +120,16 @@ class PatchesSelectorViewModel(
withContext(Dispatchers.Default) {
selectionRepository.updateSelection(appInfo.packageName, it)
}
}.mapValues { it.value.toMutableList() }.apply {
}.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental) {
return@apply
}

// Filter out unsupported patches that may have gotten selected through the database if the setting is not enabled.
bundlesFlow.first().forEach {
this[it.uid]?.removeAll(it.unsupported.map { patch -> patch.name })
}
}

init {
viewModelScope.launch(Dispatchers.Default) {
val bundles = bundlesFlow.first()
val filteredSelection =
selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) ->
// Filter out patches that don't exist.
val filteredPatches = bundles.singleOrNull { it.uid == uid }
?.let { bundle ->
val allPatches = bundle.all.map { it.name }
patches.filter { allPatches.contains(it) }
}
?: patches

filteredPatches.toMutableStateSet()
}

withContext(Dispatchers.Main) {
selectedPatches.putAll(filteredSelection)
this[it.uid]?.removeAll(it.unsupported.map { patch -> patch.name }.toSet())
}
}
}

fun getOptions(): Options = patchOptions
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
Expand Down Expand Up @@ -164,6 +171,17 @@ class PatchesSelectorViewModel(

private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
getOrPut(key, ::mutableStateMapOf)

private val optionsSaver: Saver<SnapshotStateOptions, Options> = snapshotStateMapSaver(
// Patch name -> Options
valueSaver = snapshotStateMapSaver(
// Option key -> Option value
valueSaver = snapshotStateMapSaver()
)
)

private val patchesSelectionSaver: Saver<SnapshotStatePatchesSelection, PatchesSelection> =
snapshotStateMapSaver(valueSaver = snapshotStateSetSaver())
}

data class BundleInfo(
Expand All @@ -174,4 +192,14 @@ class PatchesSelectorViewModel(
val unsupported: List<PatchInfo>,
val universal: List<PatchInfo>
)
}
}

/**
* [Options] but with observable collection types.
*/
private typealias SnapshotStateOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>

/**
* [PatchesSelection] but with observable collection types.
*/
private typealias SnapshotStatePatchesSelection = SnapshotStateMap<Int, SnapshotStateSet<String>>
12 changes: 2 additions & 10 deletions app/src/main/java/app/revanced/manager/util/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
Expand All @@ -20,10 +19,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.io.path.Path

typealias PatchesSelection = Map<Int, List<String>>
typealias PatchesSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>

fun Context.openUrl(url: String) {
Expand Down Expand Up @@ -96,9 +93,4 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) {
combiner(it)
}
}

val PathSaver = Saver<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)
}
13 changes: 13 additions & 0 deletions app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.revanced.manager.util.saver

import androidx.compose.runtime.saveable.Saver
import java.nio.file.Path
import kotlin.io.path.Path

/**
* A [Saver] that can save [Path]s. Only works with the default filesystem.
*/
val PathSaver = Saver<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package app.revanced.manager.util.saver

import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.runtime.toMutableStateList
import androidx.compose.runtime.toMutableStateMap
import app.revanced.manager.util.SnapshotStateSet
import app.revanced.manager.util.toMutableStateSet

/**
* Create a [Saver] for [SnapshotStateList]s.
*/
fun <T> snapshotStateListSaver() = Saver<SnapshotStateList<T>, List<T>>(
save = {
it.toMutableList()
},
restore = {
it.toMutableStateList()
}
)

/**
* Create a [Saver] for [SnapshotStateSet]s.
*/
fun <T> snapshotStateSetSaver() = Saver<SnapshotStateSet<T>, Set<T>>(
save = {
it.toMutableSet()
},
restore = {
it.toMutableStateSet()
}
)

/**
* Create a [Saver] for [SnapshotStateMap]s.
*/
fun <K, V> snapshotStateMapSaver() = Saver<SnapshotStateMap<K, V>, Map<K, V>>(
save = {
it.toMutableMap()
},
restore = {
mutableStateMapOf<K, V>().apply {
this.putAll(it)
}
}
)

/**
* Create a saver for [SnapshotStateMap]s with a custom [Saver] used for the values.
* Null values will not be saved by this [Saver].
*
* @param valueSaver The [Saver] used for the values of the [Map].
*/
fun <K, Original, Saveable : Any> snapshotStateMapSaver(
valueSaver: Saver<Original, Saveable>
) = Saver<SnapshotStateMap<K, Original>, Map<K, Saveable>>(
save = {
buildMap {
it.forEach { (key, value) ->
with(valueSaver) {
save(value)?.let {
this@buildMap[key] = it
}
}
}
}
},
restore = {
it.mapNotNull { (key, value) ->
valueSaver.restore(value)?.let { restored -> key to restored }
}.toMutableStateMap()
}
)

0 comments on commit 8dd8f88

Please sign in to comment.