feat: get bundle information from jar manifest (#2027)

This commit is contained in:
Ax333l 2024-07-02 21:50:28 +02:00 committed by GitHub
parent a12c5c583b
commit 1ce56af3b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 162 additions and 100 deletions

View File

@ -9,7 +9,7 @@ interface PatchBundleDao {
suspend fun all(): List<PatchBundleEntity> suspend fun all(): List<PatchBundleEntity>
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid") @Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow<BundleProperties> fun getPropsById(uid: Int): Flow<BundleProperties?>
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid") @Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
@ -17,6 +17,9 @@ interface PatchBundleDao {
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean) suspend fun setAutoUpdate(uid: Int, value: Boolean)
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
suspend fun setName(uid: Int, value: String)
@Query("DELETE FROM patch_bundles WHERE uid != 0") @Query("DELETE FROM patch_bundles WHERE uid != 0")
suspend fun purgeCustomBundles() suspend fun purgeCustomBundles()

View File

@ -7,7 +7,8 @@ import java.io.InputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) { class LocalPatchBundle(name: String, id: Int, directory: File) :
PatchBundleSource(name, id, directory) {
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) { suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
patches?.let { inputStream -> patches?.let { inputStream ->
@ -16,10 +17,16 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
} }
} }
integrations?.let { integrations?.let {
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(
it,
this@LocalPatchBundle.integrationsFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
} }
} }
reload() reload()?.also {
saveVersion(it.readManifestAttribute("Version"), null)
}
} }
} }

View File

@ -1,12 +1,20 @@
package app.revanced.manager.domain.bundles package app.revanced.manager.domain.bundles
import android.app.Application
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
@ -14,13 +22,21 @@ import java.io.OutputStream
* A [PatchBundle] source. * A [PatchBundle] source.
*/ */
@Stable @Stable
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) { sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
protected val configRepository: PatchBundlePersistenceRepository by inject()
private val app: Application by inject()
protected val patchesFile = directory.resolve("patches.jar") protected val patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk") protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(load()) private val _state = MutableStateFlow(load())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private val _nameFlow = MutableStateFlow(initialName)
val nameFlow =
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
suspend fun getName() = nameFlow.first()
/** /**
* Returns true if the bundle has been downloaded to local storage. * Returns true if the bundle has been downloaded to local storage.
*/ */
@ -42,13 +58,38 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
return try { return try {
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
} catch (t: Throwable) { } catch (t: Throwable) {
Log.e(tag, "Failed to load patch bundle $name", t) Log.e(tag, "Failed to load patch bundle with UID $uid", t)
State.Failed(t) State.Failed(t)
} }
} }
fun reload() { suspend fun reload(): PatchBundle? {
_state.value = load() val newState = load()
_state.value = newState
val bundle = newState.patchBundleOrNull()
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
if (bundle != null && _nameFlow.value.isEmpty()) {
bundle.readManifestAttribute("Name")?.let { setName(it) }
}
return bundle
}
/**
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
* The flow will emit null if the associated [PatchBundleSource] is deleted.
*/
fun propsFlow() = configRepository.getProps(uid)
suspend fun getProps() = configRepository.getProps(uid).first()!!
suspend fun currentVersion() = getProps().versionInfo
protected suspend fun saveVersion(patches: String?, integrations: String?) =
configRepository.updateVersion(uid, patches, integrations)
suspend fun setName(name: String) {
configRepository.setName(uid, name)
_nameFlow.value = name
} }
sealed interface State { sealed interface State {
@ -61,9 +102,12 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
} }
} }
companion object { companion object Extensions {
val PatchBundleSource.isDefault get() = uid == 0 val PatchBundleSource.isDefault inline get() = uid == 0
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null) val PatchBundleSource.nameState
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
""
)
} }
} }

View File

@ -2,7 +2,6 @@ package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.dto.BundleAsset import app.revanced.manager.network.dto.BundleAsset
@ -15,17 +14,14 @@ import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@Stable @Stable
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) : sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
PatchBundleSource(name, id, directory), KoinComponent { PatchBundleSource(name, id, directory) {
private val configRepository: PatchBundlePersistenceRepository by inject()
protected val http: HttpService by inject() protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): BundleInfo protected abstract suspend fun getLatestInfo(): BundleInfo
@ -70,17 +66,11 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
true true
} }
private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
private suspend fun saveVersion(patches: String, integrations: String) =
configRepository.updateVersion(uid, patches, integrations)
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete) arrayOf(patchesFile, integrationsFile).forEach(File::delete)
reload() reload()
} }
fun propsFlow() = configRepository.getProps(uid)
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
companion object { companion object {

View File

@ -22,7 +22,6 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun reset() = dao.reset() suspend fun reset() = dao.reset()
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) = suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
PatchBundleEntity( PatchBundleEntity(
uid = generateUid(), uid = generateUid(),
@ -36,17 +35,19 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid) suspend fun delete(uid: Int) = dao.remove(uid)
suspend fun updateVersion(uid: Int, patches: String, integrations: String) = suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) =
dao.updateVersion(uid, patches, integrations) dao.updateVersion(uid, patches, integrations)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged() fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
private companion object { private companion object {
val defaultSource = PatchBundleEntity( val defaultSource = PatchBundleEntity(
uid = 0, uid = 0,
name = "Default", name = "",
versionInfo = VersionInfo(), versionInfo = VersionInfo(),
source = Source.API, source = Source.API,
autoUpdate = false autoUpdate = false

View File

@ -137,16 +137,16 @@ class PatchBundleRepository(
private fun addBundle(patchBundle: PatchBundleSource) = private fun addBundle(patchBundle: PatchBundleSource) =
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) { suspend fun createLocal(patches: InputStream, integrations: InputStream?) {
val id = persistenceRepo.create(name, SourceInfo.Local).uid val uid = persistenceRepo.create("", SourceInfo.Local).uid
val bundle = LocalPatchBundle(name, id, directoryOf(id)) val bundle = LocalPatchBundle("", uid, directoryOf(uid))
bundle.replace(patches, integrations) bundle.replace(patches, integrations)
addBundle(bundle) addBundle(bundle)
} }
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) { suspend fun createRemote(url: String, autoUpdate: Boolean) {
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate) val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
addBundle(entity.load()) addBundle(entity.load())
} }
@ -174,8 +174,8 @@ class PatchBundleRepository(
getBundlesByType<RemotePatchBundle>().forEach { getBundlesByType<RemotePatchBundle>().forEach {
launch { launch {
if (!it.propsFlow().first().autoUpdate) return@launch if (!it.getProps().autoUpdate) return@launch
Log.d(tag, "Updating patch bundle: ${it.name}") Log.d(tag, "Updating patch bundle: ${it.getName()}")
it.update() it.update()
} }
} }

View File

@ -5,6 +5,8 @@ import app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import java.io.File import java.io.File
import java.io.IOException
import java.util.jar.JarFile
class PatchBundle(val patchesJar: File, val integrations: File?) { class PatchBundle(val patchesJar: File, val integrations: File?) {
private val loader = object : Iterable<Patch<*>> { private val loader = object : Iterable<Patch<*>> {
@ -25,6 +27,17 @@ class PatchBundle(val patchesJar: File, val integrations: File?) {
*/ */
val patches = loader.map(::PatchInfo) val patches = loader.map(::PatchInfo)
/**
* The [java.util.jar.Manifest] of [patchesJar].
*/
private val manifest = try {
JarFile(patchesJar).use { it.manifest }
} catch (_: IOException) {
null
}
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
/** /**
* Load all patches compatible with the specified package. * Load all patches compatible with the specified package.
*/ */

View File

@ -30,7 +30,7 @@ import app.revanced.manager.util.isDebuggable
fun BaseBundleDialog( fun BaseBundleDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDefault: Boolean, isDefault: Boolean,
name: String, name: String?,
onNameChange: ((String) -> Unit)? = null, onNameChange: ((String) -> Unit)? = null,
remoteUrl: String?, remoteUrl: String?,
onRemoteUrlChange: ((String) -> Unit)? = null, onRemoteUrlChange: ((String) -> Unit)? = null,
@ -52,32 +52,34 @@ fun BaseBundleDialog(
) )
.then(modifier) .then(modifier)
) { ) {
var showNameInputDialog by rememberSaveable { if (name != null) {
mutableStateOf(false) var showNameInputDialog by rememberSaveable {
} mutableStateOf(false)
if (showNameInputDialog) { }
TextInputDialog( if (showNameInputDialog) {
initial = name, TextInputDialog(
title = stringResource(R.string.bundle_input_name), initial = name,
onDismissRequest = { title = stringResource(R.string.bundle_input_name),
showNameInputDialog = false onDismissRequest = {
}, showNameInputDialog = false
onConfirm = { },
showNameInputDialog = false onConfirm = {
onNameChange?.invoke(it) showNameInputDialog = false
}, onNameChange?.invoke(it)
validator = { },
it.length in 1..19 validator = {
it.length in 1..19
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_input_name),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
modifier = Modifier.clickable(enabled = onNameChange != null) {
showNameInputDialog = true
} }
) )
} }
BundleListItem(
headlineText = stringResource(R.string.bundle_input_name),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
modifier = Modifier.clickable(enabled = onNameChange != null) {
showNameInputDialog = true
}
)
remoteUrl?.takeUnless { isDefault }?.let { url -> remoteUrl?.takeUnless { isDefault }?.let { url ->
var showUrlInputDialog by rememberSaveable { var showUrlInputDialog by rememberSaveable {

View File

@ -23,9 +23,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.LocalPatchBundle import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,7 +44,7 @@ fun BundleInformationDialog(
bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
}.collectAsStateWithLifecycle(0) }.collectAsStateWithLifecycle(0)
val props by remember(bundle) { val props by remember(bundle) {
bundle.propsOrNullFlow() bundle.propsFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
@ -63,10 +63,12 @@ fun BundleInformationDialog(
dismissOnBackPress = true dismissOnBackPress = true
) )
) { ) {
val bundleName by bundle.nameState
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = bundle.name, title = bundleName,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { onBackIcon = {
Icon( Icon(
@ -98,7 +100,8 @@ fun BundleInformationDialog(
BaseBundleDialog( BaseBundleDialog(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
isDefault = bundle.isDefault, isDefault = bundle.isDefault,
name = bundle.name, name = bundleName,
onNameChange = { composableScope.launch { bundle.setName(it) } },
remoteUrl = bundle.asRemoteOrNull?.endpoint, remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount, patchCount = patchCount,
version = props?.versionInfo?.patches, version = props?.versionInfo?.patches,

View File

@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -45,8 +45,9 @@ fun BundleItem(
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by remember(bundle) {
bundle.propsOrNullFlow().map { props -> props?.versionInfo?.patches } bundle.propsFlow().map { props -> props?.versionInfo?.patches }
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val name by bundle.nameState
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
BundleInformationDialog( BundleInformationDialog(
@ -77,10 +78,10 @@ fun BundleItem(
} }
} else null, } else null,
headlineContent = { Text(text = bundle.name) }, headlineContent = { Text(name) },
supportingContent = { supportingContent = {
state.patchBundleOrNull()?.patches?.size?.let { patchCount -> state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
Text(text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
} }
}, },
trailingContent = { trailingContent = {
@ -95,7 +96,7 @@ fun BundleItem(
icon?.let { (vector, description) -> icon?.let { (vector, description) ->
Icon( Icon(
imageVector = vector, vector,
contentDescription = stringResource(description), contentDescription = stringResource(description),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error

View File

@ -12,10 +12,12 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -51,6 +53,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
) )
} }
bundles.forEach { bundles.forEach {
val name by it.nameState
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@ -62,7 +65,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
} }
) { ) {
Text( Text(
text = it.name, name,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )

View File

@ -35,11 +35,10 @@ import app.revanced.manager.util.JAR_MIMETYPE
@Composable @Composable
fun ImportBundleDialog( fun ImportBundleDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onRemoteSubmit: (String, String, Boolean) -> Unit, onRemoteSubmit: (String, Boolean) -> Unit,
onLocalSubmit: (String, Uri, Uri?) -> Unit, onLocalSubmit: (Uri, Uri?) -> Unit,
initialBundleType: BundleType initialBundleType: BundleType
) { ) {
var name by rememberSaveable { mutableStateOf("") }
var remoteUrl by rememberSaveable { mutableStateOf("") } var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(true) } var autoUpdate by rememberSaveable { mutableStateOf(true) }
var bundleType by rememberSaveable { mutableStateOf(initialBundleType) } var bundleType by rememberSaveable { mutableStateOf(initialBundleType) }
@ -48,7 +47,7 @@ fun ImportBundleDialog(
val inputsAreValid by remember { val inputsAreValid by remember {
derivedStateOf { derivedStateOf {
name.isNotEmpty() && if (bundleType == BundleType.Local) patchBundle != null else remoteUrl.isNotEmpty() if (bundleType == BundleType.Local) patchBundle != null else remoteUrl.isNotEmpty()
} }
} }
@ -56,6 +55,7 @@ fun ImportBundleDialog(
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { patchBundle = it } uri?.let { patchBundle = it }
} }
fun launchPatchActivity() { fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE) patchActivityLauncher.launch(JAR_MIMETYPE)
} }
@ -64,6 +64,7 @@ fun ImportBundleDialog(
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it } uri?.let { integrations = it }
} }
fun launchIntegrationsActivity() { fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE) integrationsActivityLauncher.launch(APK_MIMETYPE)
} }
@ -91,12 +92,8 @@ fun ImportBundleDialog(
enabled = inputsAreValid, enabled = inputsAreValid,
onClick = { onClick = {
when (bundleType) { when (bundleType) {
BundleType.Local -> onLocalSubmit( BundleType.Local -> onLocalSubmit(patchBundle!!, integrations)
name, BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
patchBundle!!,
integrations
)
BundleType.Remote -> onRemoteSubmit(name, remoteUrl, autoUpdate)
} }
}, },
modifier = Modifier.padding(end = 16.dp) modifier = Modifier.padding(end = 16.dp)
@ -110,8 +107,7 @@ fun ImportBundleDialog(
BaseBundleDialog( BaseBundleDialog(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
isDefault = false, isDefault = false,
name = name, name = null,
onNameChange = { name = it },
remoteUrl = remoteUrl.takeUnless { bundleType == BundleType.Local }, remoteUrl = remoteUrl.takeUnless { bundleType == BundleType.Local },
onRemoteUrlChange = { remoteUrl = it }, onRemoteUrlChange = { remoteUrl = it },
patchCount = 0, patchCount = 0,

View File

@ -75,7 +75,7 @@ data class BundleInfo(
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.name, source.uid, supported, unsupported, universal) BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
} }
} }
} }

View File

@ -46,7 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
@ -102,13 +102,13 @@ fun DashboardScreen(
ImportBundleDialog( ImportBundleDialog(
onDismissRequest = ::dismiss, onDismissRequest = ::dismiss,
onLocalSubmit = { name, patches, integrations -> onLocalSubmit = { patches, integrations ->
dismiss() dismiss()
vm.createLocalSource(name, patches, integrations) vm.createLocalSource(patches, integrations)
}, },
onRemoteSubmit = { name, url, autoUpdate -> onRemoteSubmit = { url, autoUpdate ->
dismiss() dismiss()
vm.createRemoteSource(name, url, autoUpdate) vm.createRemoteSource(url, autoUpdate)
}, },
initialBundleType = it initialBundleType = it
) )

View File

@ -12,7 +12,7 @@ import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
@ -80,20 +80,17 @@ class DashboardViewModel(
fun cancelSourceSelection() { fun cancelSourceSelection() {
selectedSources.clear() selectedSources.clear()
} }
fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = fun createLocalSource(patchBundle: Uri, integrations: Uri?) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
val integrationsStream = integrations?.let { contentResolver.openInputStream(it) } integrations?.let { contentResolver.openInputStream(it) }.use { integrationsStream ->
try { patchBundleRepository.createLocal(patchesStream, integrationsStream)
patchBundleRepository.createLocal(name, patchesStream, integrationsStream)
} finally {
integrationsStream?.close()
} }
} }
} }
fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) = fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) =
viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) } viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) }
fun delete(bundle: PatchBundleSource) = fun delete(bundle: PatchBundleSource) =
viewModelScope.launch { patchBundleRepository.remove(bundle) } viewModelScope.launch { patchBundleRepository.remove(bundle) }
@ -107,9 +104,9 @@ class DashboardViewModel(
RemotePatchBundle.updateFailMsg RemotePatchBundle.updateFailMsg
) { ) {
if (bundle.update()) if (bundle.update())
app.toast(app.getString(R.string.bundle_update_success, bundle.name)) app.toast(app.getString(R.string.bundle_update_success, bundle.getName()))
else else
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.name)) app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
} }
} }
} }

View File

@ -11,7 +11,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository

View File

@ -25,6 +25,8 @@
<string name="bundle_missing">Missing</string> <string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="bundle_error">Error</string>
<string name="bundle_name_default">Default</string>
<string name="bundle_name_fallback">Unnamed</string>
<string name="selected_app_meta">%1$s • %2$d available patches</string> <string name="selected_app_meta">%1$s • %2$d available patches</string>