Skip to content
This repository has been archived by the owner on Nov 20, 2023. It is now read-only.

Commit

Permalink
feat: json bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 committed Aug 1, 2023
1 parent 565a3ee commit 842b9e9
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import java.io.File

class Converters {
@TypeConverter
fun sourceFromString(value: String) = when(value) {
Source.Local.SENTINEL -> Source.Local
else -> Source.Remote(Url(value))
}
fun sourceFromString(value: String) = Source.from(value)

@TypeConverter
fun sourceToString(value: Source) = value.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@ sealed class Source {
override fun toString() = SENTINEL
}

object API : Source() {
const val SENTINEL = "manager://api"

override fun toString() = SENTINEL
}

data class Remote(val url: Url) : Source() {
override fun toString() = url.toString()
}

companion object {
fun from(value: String) = when(value) {
Local.SENTINEL -> Local
API.SENTINEL -> API
else -> Remote(Url(value))
}
}
}

data class VersionInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
withContext(Dispatchers.IO) {
patches?.let {
Files.copy(it, patchesJar.toPath(), StandardCopyOption.REPLACE_EXISTING)
Files.copy(it, patchesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
integrations?.let {
Files.copy(it, this@LocalPatchBundle.integrations.toPath(), StandardCopyOption.REPLACE_EXISTING)
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import java.io.File
*/
@Stable
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) {
protected val patchesJar = directory.resolve("patches.jar")
protected val integrations = directory.resolve("integrations.apk")
protected val patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk")

/**
* Returns true if the bundle has been downloaded to local storage.
*/
fun hasInstalled() = patchesJar.exists()
fun hasInstalled() = patchesFile.exists()

private fun load(): State {
if (!hasInstalled()) return State.Missing

return try {
State.Loaded(PatchBundle(patchesJar, integrations.takeIf(File::exists)))
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
} catch (t: Throwable) {
State.Failed(t)
}
Expand All @@ -48,6 +48,7 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)

companion object {
val PatchBundleSource.isDefault get() = uid == 0
fun PatchBundleSource.propsOrNullFlow() = (this as? RemotePatchBundle)?.propsFlow() ?: flowOf(null)
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle<*>
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null)
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
package app.revanced.manager.domain.bundles

import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.domain.repository.Assets
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.dto.BundleInfo
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.ghIntegrations
import app.revanced.manager.util.ghPatches
import io.ktor.client.request.url
import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File

@Stable
class RemotePatchBundle(name: String, id: Int, directory: File, val apiUrl: String) :
sealed class RemotePatchBundle<Meta>(name: String, id: Int, directory: File, val endpoint: String) :
PatchBundleSource(name, id, directory), KoinComponent {
private val configRepository: PatchBundlePersistenceRepository by inject()
private val api: ManagerAPI by inject()
protected val http: HttpService by inject()

private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
private suspend fun saveVersion(patches: String, integrations: String) =
configRepository.updateVersion(uid, patches, integrations)
protected abstract suspend fun download(metadata: Meta)
protected abstract suspend fun getLatestMetadata(): Meta
protected abstract fun getVersionInfo(metadata: Meta): VersionInfo

suspend fun downloadLatest() = withContext(Dispatchers.IO) {
api.downloadBundle(apiUrl, patchesJar, integrations).also { (patchesVer, integrationsVer) ->
saveVersion(patchesVer, integrationsVer)
reload()
}
suspend fun downloadLatest() {
download(getLatestMetadata())
}

return@withContext
protected suspend fun downloadAssets(assets: Map<String, File>) = coroutineScope {
assets.forEach { (asset, file) ->
launch {
http.download(file) {
url(asset)
}
}
}
}

suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val current = currentVersion().let { it.patches to it.integrations }
if (!hasInstalled() || current != api.getLatestBundleVersion(apiUrl)) {
downloadLatest()
true
} else false
val metadata = getLatestMetadata()
if (hasInstalled() && getVersionInfo(metadata) == currentVersion()) {
return@withContext false
}

download(metadata)
true
}

suspend fun deleteLocalFiles() = withContext(Dispatchers.IO) {
arrayOf(patchesJar, integrations).forEach(File::delete)
private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
protected suspend fun saveVersion(patches: String, integrations: String) =
configRepository.updateVersion(uid, patches, integrations)

suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete)
reload()
}

Expand All @@ -49,4 +71,60 @@ class RemotePatchBundle(name: String, id: Int, directory: File, val apiUrl: Stri
companion object {
const val updateFailMsg = "Failed to update patch bundle(s)"
}
}

class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle<BundleInfo>(name, id, directory, endpoint) {
override suspend fun getLatestMetadata() = withContext(Dispatchers.IO) {
http.request<BundleInfo> {
url(endpoint)
}.getOrThrow()
}

override fun getVersionInfo(metadata: BundleInfo) =
VersionInfo(metadata.patches.version, metadata.integrations.version)

override suspend fun download(metadata: BundleInfo) = withContext(Dispatchers.IO) {
val (patches, integrations) = metadata
downloadAssets(
mapOf(
patches.url to patchesFile,
integrations.url to integrationsFile
)
)

saveVersion(patches.version, integrations.version)
reload()
}
}

class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle<Assets>(name, id, directory, endpoint) {
private val api: ReVancedRepository by inject()

override suspend fun getLatestMetadata() = api.getAssets()
override fun getVersionInfo(metadata: Assets) = metadata.let { (patches, integrations) ->
VersionInfo(
patches.version,
integrations.version
)
}

override suspend fun download(metadata: Assets) = withContext(Dispatchers.IO) {
val (patches, integrations) = metadata
downloadAssets(
mapOf(
patches.downloadUrl to patchesFile,
integrations.downloadUrl to integrationsFile
)
)

saveVersion(patches.version, integrations.version)
reload()
}

private companion object {
operator fun Assets.component1() = find(ghPatches, ".jar")
operator fun Assets.component2() = find(ghIntegrations, ".apk")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
uid = 0,
name = "Main",
versionInfo = VersionInfo(),
source = Source.Remote(Url("manager://api")),
source = Source.API,
autoUpdate = false
)
}
Expand All @@ -33,25 +33,23 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {

suspend fun reset() = dao.reset()

suspend fun create(name: String, source: Source, autoUpdate: Boolean = false): Int {
val uid = generateUid()
dao.add(
PatchBundleEntity(
uid = uid,
name = name,
versionInfo = VersionInfo(),
source = source,
autoUpdate = autoUpdate
)
)

return uid
}
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
PatchBundleEntity(
uid = generateUid(),
name = name,
versionInfo = VersionInfo(),
source = source,
autoUpdate = autoUpdate
).also {
dao.add(it)
}

suspend fun delete(uid: Int) = dao.remove(uid)

suspend fun updateVersion(uid: Int, patches: String, integrations: String) =
dao.updateVersion(uid, patches, integrations)

suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)

fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import android.content.Context
import android.util.Log
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.domain.bundles.APIPatchBundle
import app.revanced.manager.domain.bundles.JsonPatchBundle
import app.revanced.manager.data.room.bundles.Source as SourceInfo
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.util.flatMapLatestAndCombine
import app.revanced.manager.util.tag
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
Expand All @@ -21,18 +21,17 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream

class PatchBundleRepository(
app: Application,
private val persistenceRepo: PatchBundlePersistenceRepository,
private val networkInfo: NetworkInfo,
private val prefs: PreferencesManager
) {
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)

private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> = MutableStateFlow(emptyMap())
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
MutableStateFlow(emptyMap())
val sources = _sources.map { it.values.toList() }

val bundles = sources.flatMapLatestAndCombine(
Expand All @@ -51,14 +50,19 @@ class PatchBundleRepository(
*/
private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() }

private suspend fun PatchBundleEntity.load(dir: File) = when (source) {
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
is SourceInfo.Remote -> RemotePatchBundle(
name,
uid,
dir,
if (uid != 0) source.url.toString() else prefs.api.get()
)
private fun PatchBundleEntity.load(): PatchBundleSource {
val dir = directoryOf(uid)

return when (source) {
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
is SourceInfo.Remote -> JsonPatchBundle(
name,
uid,
dir,
source.url.toString()
)
}
}

suspend fun load() = withContext(Dispatchers.Default) {
Expand All @@ -67,10 +71,7 @@ class PatchBundleRepository(
}

_sources.value = entities.associate {
val dir = directoryOf(it.uid)
val bundle = it.load(dir)

it.uid to bundle
it.uid to it.load()
}
}

Expand Down Expand Up @@ -100,23 +101,26 @@ class PatchBundleRepository(
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }

suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) {
val id = persistenceRepo.create(name, SourceInfo.Local)
val source = LocalPatchBundle(name, id, directoryOf(id))

addBundle(source)
val id = persistenceRepo.create(name, SourceInfo.Local).uid
val bundle = LocalPatchBundle(name, id, directoryOf(id))

source.replace(patches, integrations)
bundle.replace(patches, integrations)
addBundle(bundle)
}

suspend fun createRemote(name: String, apiUrl: Url, autoUpdate: Boolean) {
val id = persistenceRepo.create(name, SourceInfo.Remote(apiUrl), autoUpdate)
addBundle(RemotePatchBundle(name, id, directoryOf(id), apiUrl.toString()))
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) {
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate)
addBundle(entity.load())
}

private suspend fun getRemoteBundles() = sources.first().filterIsInstance<RemotePatchBundle>()
private suspend fun getRemoteBundles() =
sources.first().filterIsInstance<RemotePatchBundle<*>>()

suspend fun reloadApiBundles() {
sources.first().filterIsInstance<APIPatchBundle>().forEach {
it.deleteLocalFiles()
}

suspend fun reloadDefaultBundle() {
_sources.value[0]?.let { it as? RemotePatchBundle }?.deleteLocalFiles()
load()
}

Expand Down
Loading

0 comments on commit 842b9e9

Please sign in to comment.