Skip to content

Commit af8e2b4

Browse files
Axelen123oSumAtrIX
authored andcommitted
feat: allow bundles to use classes from other bundles (#1951)
1 parent a8820a4 commit af8e2b4

36 files changed

+1304
-1042
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package app.revanced.manager.data.redux
2+
3+
import android.util.Log
4+
import app.revanced.manager.util.tag
5+
import kotlinx.coroutines.CancellationException
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.ExperimentalCoroutinesApi
8+
import kotlinx.coroutines.channels.Channel
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.asStateFlow
11+
import kotlinx.coroutines.launch
12+
import kotlinx.coroutines.sync.Mutex
13+
import kotlinx.coroutines.sync.withLock
14+
import kotlinx.coroutines.withTimeoutOrNull
15+
16+
// This file implements React Redux-like state management.
17+
18+
class Store<S>(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext {
19+
private val _state = MutableStateFlow(initialState)
20+
val state = _state.asStateFlow()
21+
22+
// Do not touch these without the lock.
23+
private var isRunningActions = false
24+
private val queueChannel = Channel<Action<S>>(capacity = 10)
25+
private val lock = Mutex()
26+
27+
suspend fun dispatch(action: Action<S>) = lock.withLock {
28+
Log.d(tag, "Dispatching $action")
29+
queueChannel.send(action)
30+
31+
if (isRunningActions) return@withLock
32+
isRunningActions = true
33+
coroutineScope.launch {
34+
runActions()
35+
}
36+
}
37+
38+
@OptIn(ExperimentalCoroutinesApi::class)
39+
private suspend fun runActions() {
40+
while (true) {
41+
val action = withTimeoutOrNull(200L) { queueChannel.receive() }
42+
if (action == null) {
43+
Log.d(tag, "Stopping action runner")
44+
lock.withLock {
45+
// New actions may be dispatched during the timeout.
46+
isRunningActions = !queueChannel.isEmpty
47+
if (!isRunningActions) return
48+
}
49+
continue
50+
}
51+
52+
Log.d(tag, "Running $action")
53+
_state.value = try {
54+
with(action) { this@Store.execute(_state.value) }
55+
} catch (c: CancellationException) {
56+
// This is done without the lock, but cancellation usually means the store is no longer needed.
57+
isRunningActions = false
58+
throw c
59+
} catch (e: Exception) {
60+
action.catch(e)
61+
continue
62+
}
63+
}
64+
}
65+
}
66+
67+
interface ActionContext
68+
69+
interface Action<S> {
70+
suspend fun ActionContext.execute(current: S): S
71+
suspend fun catch(exception: Exception) {
72+
Log.e(tag, "Got exception while executing $this", exception)
73+
}
74+
}
Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
package app.revanced.manager.data.room.bundles
22

33
import androidx.room.*
4-
import kotlinx.coroutines.flow.Flow
54

65
@Dao
76
interface PatchBundleDao {
87
@Query("SELECT * FROM patch_bundles")
98
suspend fun all(): List<PatchBundleEntity>
109

11-
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
12-
fun getPropsById(uid: Int): Flow<BundleProperties?>
13-
1410
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
1511
suspend fun updateVersionHash(uid: Int, patches: String?)
1612

17-
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
18-
suspend fun setAutoUpdate(uid: Int, value: Boolean)
19-
20-
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
21-
suspend fun setName(uid: Int, value: String)
22-
2313
@Query("DELETE FROM patch_bundles WHERE uid != 0")
2414
suspend fun purgeCustomBundles()
2515

@@ -32,6 +22,9 @@ interface PatchBundleDao {
3222
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
3323
suspend fun remove(uid: Int)
3424

35-
@Insert
36-
suspend fun add(source: PatchBundleEntity)
25+
@Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid")
26+
suspend fun getProps(uid: Int): PatchBundleProperties?
27+
28+
@Upsert
29+
suspend fun upsert(source: PatchBundleEntity)
3730
}

app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ data class PatchBundleEntity(
3838
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
3939
)
4040

41-
data class BundleProperties(
41+
data class PatchBundleProperties(
42+
@ColumnInfo(name = "name") val name: String,
4243
@ColumnInfo(name = "version") val versionHash: String? = null,
44+
@ColumnInfo(name = "source") val source: Source,
4345
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
4446
)

app/src/main/java/app/revanced/manager/di/RepositoryModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ val repositoryModule = module {
1515
createdAtStart()
1616
}
1717
singleOf(::NetworkInfo)
18-
singleOf(::PatchBundlePersistenceRepository)
1918
singleOf(::PatchSelectionRepository)
2019
singleOf(::PatchOptionsRepository)
2120
singleOf(::PatchBundleRepository) {

app/src/main/java/app/revanced/manager/di/ViewModelModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ val viewModelModule = module {
2323
viewModelOf(::InstalledAppsViewModel)
2424
viewModelOf(::InstalledAppInfoViewModel)
2525
viewModelOf(::UpdatesSettingsViewModel)
26+
viewModelOf(::BundleListViewModel)
2627
}
Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
package app.revanced.manager.domain.bundles
22

3+
import app.revanced.manager.data.redux.ActionContext
34
import kotlinx.coroutines.Dispatchers
45
import kotlinx.coroutines.withContext
56
import java.io.File
67
import java.io.InputStream
78

8-
class LocalPatchBundle(name: String, id: Int, directory: File) :
9-
PatchBundleSource(name, id, directory) {
10-
suspend fun replace(patches: InputStream) {
9+
class LocalPatchBundle(
10+
name: String,
11+
uid: Int,
12+
error: Throwable?,
13+
directory: File
14+
) : PatchBundleSource(name, uid, error, directory) {
15+
suspend fun ActionContext.replace(patches: InputStream) {
1116
withContext(Dispatchers.IO) {
1217
patchBundleOutputStream().use { outputStream ->
1318
patches.copyTo(outputStream)
1419
}
1520
}
16-
17-
reload()?.also {
18-
saveVersionHash(it.patchBundleManifestAttributes?.version)
19-
}
2021
}
22+
23+
override fun copy(error: Throwable?, name: String) = LocalPatchBundle(
24+
name,
25+
uid,
26+
error,
27+
directory
28+
)
2129
}
Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,43 @@
11
package app.revanced.manager.domain.bundles
22

3-
import android.app.Application
4-
import android.util.Log
5-
import androidx.compose.runtime.Composable
63
import androidx.compose.runtime.Stable
7-
import androidx.lifecycle.compose.collectAsStateWithLifecycle
8-
import app.revanced.manager.R
9-
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
4+
import app.revanced.manager.data.redux.ActionContext
105
import app.revanced.manager.patcher.patch.PatchBundle
11-
import app.revanced.manager.util.tag
126
import kotlinx.coroutines.Dispatchers
13-
import kotlinx.coroutines.flow.MutableStateFlow
14-
import kotlinx.coroutines.flow.asStateFlow
15-
import kotlinx.coroutines.flow.first
16-
import kotlinx.coroutines.flow.flowOn
17-
import kotlinx.coroutines.flow.map
18-
import org.koin.core.component.KoinComponent
19-
import org.koin.core.component.inject
7+
import kotlinx.coroutines.withContext
208
import java.io.File
219
import java.io.OutputStream
2210

2311
/**
2412
* A [PatchBundle] source.
2513
*/
2614
@Stable
27-
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
28-
protected val configRepository: PatchBundlePersistenceRepository by inject()
29-
private val app: Application by inject()
15+
sealed class PatchBundleSource(
16+
val name: String,
17+
val uid: Int,
18+
error: Throwable?,
19+
protected val directory: File
20+
) {
3021
protected val patchesFile = directory.resolve("patches.jar")
3122

32-
private val _state = MutableStateFlow(load())
33-
val state = _state.asStateFlow()
23+
val state = when {
24+
error != null -> State.Failed(error)
25+
!hasInstalled() -> State.Missing
26+
else -> State.Available(PatchBundle(patchesFile.absolutePath))
27+
}
3428

35-
private val _nameFlow = MutableStateFlow(initialName)
36-
val nameFlow =
37-
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.patches_name_default else R.string.patches_name_fallback) } }
29+
val patchBundle get() = (state as? State.Available)?.bundle
30+
val version get() = patchBundle?.manifestAttributes?.version
31+
val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true
32+
val error get() = (state as? State.Failed)?.throwable
3833

39-
suspend fun getName() = nameFlow.first()
34+
suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) {
35+
patchesFile.delete()
36+
}
4037

41-
val versionFlow = state.map { it.patchBundleOrNull()?.patchBundleManifestAttributes?.version }
42-
val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
38+
abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource
4339

44-
/**
45-
* Returns true if the bundle has been downloaded to local storage.
46-
*/
47-
fun hasInstalled() = patchesFile.exists()
40+
protected fun hasInstalled() = patchesFile.exists()
4841

4942
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
5043
// Android 14+ requires dex containers to be readonly.
@@ -56,62 +49,14 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
5649
}
5750
}
5851

59-
private fun load(): State {
60-
if (!hasInstalled()) return State.Missing
61-
62-
return try {
63-
State.Loaded(PatchBundle(patchesFile))
64-
} catch (t: Throwable) {
65-
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
66-
State.Failed(t)
67-
}
68-
}
69-
70-
suspend fun reload(): PatchBundle? {
71-
val newState = load()
72-
_state.value = newState
73-
74-
val bundle = newState.patchBundleOrNull()
75-
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
76-
if (bundle != null && _nameFlow.value.isEmpty()) {
77-
bundle.patchBundleManifestAttributes?.name?.let { setName(it) }
78-
}
79-
80-
return bundle
81-
}
82-
83-
/**
84-
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
85-
* The flow will emit null if the associated [PatchBundleSource] is deleted.
86-
*/
87-
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
88-
suspend fun getProps() = propsFlow().first()!!
89-
90-
suspend fun currentVersionHash() = getProps().versionHash
91-
protected suspend fun saveVersionHash(version: String?) =
92-
configRepository.updateVersionHash(uid, version)
93-
94-
suspend fun setName(name: String) {
95-
configRepository.setName(uid, name)
96-
_nameFlow.value = name
97-
}
98-
9952
sealed interface State {
100-
fun patchBundleOrNull(): PatchBundle? = null
101-
10253
data object Missing : State
10354
data class Failed(val throwable: Throwable) : State
104-
data class Loaded(val bundle: PatchBundle) : State {
105-
override fun patchBundleOrNull() = bundle
106-
}
55+
data class Available(val bundle: PatchBundle) : State
10756
}
10857

10958
companion object Extensions {
11059
val PatchBundleSource.isDefault inline get() = uid == 0
11160
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
112-
val PatchBundleSource.nameState
113-
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
114-
""
115-
)
11661
}
11762
}

0 commit comments

Comments
 (0)