feat: Multiple downloader per APK

This commit is contained in:
oSumAtrIX
2026-01-08 23:35:05 +01:00
parent 8d0ee814fc
commit 6d9fd1aa36
12 changed files with 168 additions and 144 deletions

View File

@@ -7,8 +7,6 @@ import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.Scope
import app.revanced.manager.downloader.Downloader
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.webview.IWebView
import app.revanced.manager.downloader.webview.IWebViewEvents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@@ -7,7 +7,7 @@ import android.util.Log
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.downloader.TrustedDownloader
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.downloader.DownloaderState
import app.revanced.manager.network.downloader.DownloaderPackageState
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.downloader.DownloaderBuilder
@@ -33,93 +33,98 @@ class DownloaderRepository(
db: AppDatabase
) {
private val trustDao = db.trustedDownloaderDao()
private val _downloaderStates = MutableStateFlow(emptyMap<String, DownloaderState>())
val downloaderStates = _downloaderStates.asStateFlow()
val loadedDownloaderFlow = downloaderStates.map { states ->
states.values.filterIsInstance<DownloaderState.Loaded>().map { it.downloader }
private val _downloaderPackageStates = MutableStateFlow(emptyMap<String, DownloaderPackageState>())
val downloaderPackageStates = _downloaderPackageStates.asStateFlow()
val loadedDownloaderPackageFlow = downloaderPackageStates.map { states ->
states.values.filterIsInstance<DownloaderPackageState.Loaded>().flatMap { it.downloader }
}
private val acknowledgedDownloader = prefs.acknowledgedDownloader
private val acknowledgedPackageDownloader = prefs.acknowledgedDownloader
private val installedDownloaderPackageNames = MutableStateFlow(emptySet<String>())
val newDownloaderPackageNames = combine(
installedDownloaderPackageNames,
acknowledgedDownloader.flow
acknowledgedPackageDownloader.flow
) { installed, acknowledged ->
installed subtract acknowledged
}
suspend fun reload() {
val downloader =
val downloaderPackages =
withContext(Dispatchers.IO) {
pm.getPackagesWithFeature(DOWNLOADER_FEATURE)
.associate { it.packageName to loadDownloader(it.packageName) }
}
_downloaderStates.value = downloader
installedDownloaderPackageNames.value = downloader.keys
_downloaderPackageStates.value = downloaderPackages
installedDownloaderPackageNames.value = downloaderPackages.keys
val acknowledgedDownloader = this@DownloaderRepository.acknowledgedDownloader.get()
val acknowledgedDownloader = this@DownloaderRepository.acknowledgedPackageDownloader.get()
val uninstalledDownloader = acknowledgedDownloader subtract installedDownloaderPackageNames.value
if (uninstalledDownloader.isNotEmpty()) {
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
this@DownloaderRepository.acknowledgedDownloader.update(acknowledgedDownloader subtract uninstalledDownloader)
this@DownloaderRepository.acknowledgedPackageDownloader.update(acknowledgedDownloader subtract uninstalledDownloader)
trustDao.removeAll(uninstalledDownloader)
}
}
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloader, Parcelable> {
val downloader =
(_downloaderStates.value[data.downloaderPackageName] as? DownloaderState.Loaded)?.downloader
?: throw Exception("Downloader with name ${data.downloaderPackageName} is not available")
(_downloaderPackageStates.value[data.downloaderPackageName] as? DownloaderPackageState.Loaded)
?.downloader?.first { it.name == data.downloaderName }
?: throw Exception("Downloader package name ${data.downloaderPackageName} is not available")
return downloader to data.unwrapWith(downloader)
}
private suspend fun loadDownloader(packageName: String): DownloaderState {
private suspend fun loadDownloader(packageName: String): DownloaderPackageState {
try {
if (!verify(packageName)) return DownloaderState.Untrusted
if (!verify(packageName)) return DownloaderPackageState.Untrusted
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(tag, "Got exception while verifying downloader $packageName", e)
return DownloaderState.Failed(e)
return DownloaderPackageState.Failed(e)
}
return try {
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_DOWNLOADER_CLASS)
?: throw Exception("Missing metadata attribute $METADATA_DOWNLOADER_CLASS")
val classNames = packageInfo.applicationInfo!!.metaData.getStringArray(METADATA_DOWNLOADER_CLASSES)
?: throw Exception("Missing metadata attribute $METADATA_DOWNLOADER_CLASSES")
val classLoader =
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
val downloaderContext = app.createPackageContext(packageName, 0)
val downloader = classLoader
.loadClass(className)
.getDownloaderBuilder()
.build(
scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloaderContext.packageName
},
context = downloaderContext
)
val scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloaderContext.packageName
}
DownloaderState.Loaded(
LoadedDownloader(
packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName!!,
downloader.get,
downloader.download,
classLoader
)
DownloaderPackageState.Loaded(
classNames.map { className ->
val downloader = classLoader
.loadClass(className)
.getDownloaderBuilder()
.build(
scopeImpl = scopeImpl,
context = downloaderContext
)
LoadedDownloader(
packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName!!,
downloader.get,
downloader.download,
classLoader
)
}
)
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e(tag, "Failed to load downloader $packageName", t)
DownloaderState.Failed(t)
DownloaderPackageState.Failed(t)
}
}
@@ -133,7 +138,7 @@ class DownloaderRepository(
reload()
prefs.edit {
acknowledgedDownloader += packageName
acknowledgedPackageDownloader += packageName
}
}
@@ -141,7 +146,7 @@ class DownloaderRepository(
trustDao.remove(packageName).also { reload() }
suspend fun acknowledgeAllNewDownloader() =
acknowledgedDownloader.update(installedDownloaderPackageNames.value)
acknowledgedPackageDownloader.update(installedDownloaderPackageNames.value)
private suspend fun verify(packageName: String): Boolean {
val expectedSignature =
@@ -152,7 +157,7 @@ class DownloaderRepository(
private companion object {
const val DOWNLOADER_FEATURE = "app.revanced.manager.downloader"
const val METADATA_DOWNLOADER_CLASS = "app.revanced.manager.downloader.class"
const val METADATA_DOWNLOADER_CLASSES = "app.revanced.manager.downloader.classes"
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC

View File

@@ -0,0 +1,9 @@
package app.revanced.manager.network.downloader
sealed interface DownloaderPackageState {
data object Untrusted : DownloaderPackageState
data class Loaded(val downloader: List<LoadedDownloader>) : DownloaderPackageState
data class Failed(val throwable: Throwable) : DownloaderPackageState
}

View File

@@ -1,9 +0,0 @@
package app.revanced.manager.network.downloader
sealed interface DownloaderState {
data object Untrusted : DownloaderState
data class Loaded(val downloader: LoadedDownloader) : DownloaderState
data class Failed(val throwable: Throwable) : DownloaderState
}

View File

@@ -11,10 +11,12 @@ import kotlinx.parcelize.Parcelize
*/
class ParceledDownloaderData private constructor(
val downloaderPackageName: String,
val downloaderName: String,
private val bundle: Bundle
) : Parcelable {
constructor(downloader: LoadedDownloader, data: Parcelable) : this(
downloader.packageName,
downloader.name,
createBundle(data)
)

View File

@@ -182,7 +182,7 @@ class PatcherWorker(
is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) {
downloaderRepository.loadedDownloaderFlow.first()
downloaderRepository.loadedDownloaderPackageFlow.first()
.firstNotNullOfOrNull { downloader ->
try {
val getScope = object : GetScope {
@@ -250,11 +250,21 @@ class PatcherWorker(
tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
)
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure()
} catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e)
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
args.onEvent(
ProgressEvent.Failed(
null,
e.toRemoteError()
)
) // Fallback if exception doesn't occur within step
Result.failure()
} finally {
patchedApk.delete()

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.ui.screen
import android.R.attr.name
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
@@ -201,7 +202,7 @@ fun SelectedAppInfoScreen(
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
downloader.find { it.packageName == app.data.downloaderPackageName }?.name
downloader.find { it.packageName == app.data.downloaderPackageName && it.name == app.data.downloaderName }?.let { "${it.packageName} ${it.name}" }
?: app.data.downloaderPackageName
)

View File

@@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.network.downloader.DownloaderState
import app.revanced.manager.network.downloader.DownloaderPackageState
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ConfirmDialog
@@ -102,98 +102,105 @@ fun DownloadsSettingsScreen(
item {
GroupHeader(stringResource(R.string.downloader))
}
downloaderStates.forEach { (packageName, state) ->
item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
fun dismiss() {
showDialog = false
}
if (downloaderStates.isNotEmpty()) {
downloaderStates.forEach { (packageName, state) ->
item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
val packageInfo =
remember(packageName) {
viewModel.pm.getPackageInfo(
packageName
)
} ?: return@item
fun dismiss() {
showDialog = false
}
if (showDialog) {
val signature =
val packageInfo =
remember(packageName) {
val androidSignature =
viewModel.pm.getSignature(packageName)
val hash = MessageDigest.getInstance("SHA-256")
.digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase)
viewModel.pm.getPackageInfo(
packageName
)
} ?: return@item
if (showDialog) {
val signature =
remember(packageName) {
val androidSignature =
viewModel.pm.getSignature(packageName)
val hash = MessageDigest.getInstance("SHA-256")
.digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase)
}
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
when (state) {
is DownloaderState.Loaded -> TrustDialog(
title = R.string.downloader_revoke_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body,
packageName,
signature
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.revokeDownloaderTrust(packageName)
dismiss()
}
)
is DownloaderState.Failed -> ExceptionViewerDialog(
text = remember(state.throwable) {
state.throwable.stackTraceToString()
},
onDismiss = ::dismiss
)
is DownloaderState.Untrusted -> TrustDialog(
title = R.string.downloader_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustDownloader(packageName)
dismiss()
}
)
}
}
SettingsListItem(
modifier = Modifier.clickable { showDialog = true },
headlineContent = {
AppLabel(
packageInfo = packageInfo,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = stringResource(
when (state) {
is DownloaderState.Loaded -> R.string.downloader_state_trusted
is DownloaderState.Failed -> R.string.downloader_state_failed
is DownloaderState.Untrusted -> R.string.downloader_state_untrusted
is DownloaderPackageState.Loaded -> TrustDialog(
title = R.string.downloader_revoke_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body,
packageName,
signature
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.revokeDownloaderTrust(packageName)
dismiss()
}
)
is DownloaderPackageState.Failed -> ExceptionViewerDialog(
text = remember(state.throwable) {
state.throwable.stackTraceToString()
},
onDismiss = ::dismiss
)
is DownloaderPackageState.Untrusted -> TrustDialog(
title = R.string.downloader_trust_dialog_title,
body = stringResource(
R.string.downloader_trust_dialog_body
),
downloaderName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustDownloader(packageName)
dismiss()
}
)
}
),
trailingContent = { Text(packageInfo.versionName!!) }
)
}
SettingsListItem(
modifier = Modifier.clickable { showDialog = true },
headlineContent = {
AppLabel(
packageInfo = packageInfo,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = when (state) {
is DownloaderPackageState.Loaded -> {
val names = state.downloader.joinToString("\n") { it.name }
if (names.isNotEmpty())
stringResource(R.string.downloader_state_trusted, "\n\n$names")
else
stringResource(R.string.downloader_state_trusted)
}
is DownloaderPackageState.Failed -> stringResource(R.string.downloader_state_failed)
is DownloaderPackageState.Untrusted -> stringResource(R.string.downloader_state_untrusted)
},
trailingContent = { Text(packageInfo.versionName!!) }
)
}
}
}
if (downloaderStates.isEmpty()) {
} else {
item {
Text(
stringResource(R.string.no_downloader_installed),

View File

@@ -21,7 +21,7 @@ class DownloadsViewModel(
private val downloaderRepository: DownloaderRepository,
val pm: PM
) : ViewModel() {
val downloaderStates = downloaderRepository.downloaderStates
val downloaderStates = downloaderRepository.downloaderPackageStates
val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps ->
downloadedApps.sortedWith(
compareBy<DownloadedApp> {

View File

@@ -73,7 +73,7 @@ class SelectedAppInfoViewModel(
private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get()
val downloader = downloaderRepository.loadedDownloaderFlow
val downloader = downloaderRepository.loadedDownloaderPackageFlow
val desiredVersion = input.app.version
val packageName = input.app.packageName

View File

@@ -176,7 +176,7 @@ You will not be able to update the previously installed apps from this source."<
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
<string name="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader">Downloader</string>
<string name="downloader_state_trusted">Trusted</string>
<string name="downloader_state_trusted">Trusted%s</string>
<string name="downloader_state_failed">Failed to load. Click for more details</string>
<string name="downloader_state_untrusted">Untrusted</string>
<string name="downloader_trust_dialog_title">Trust downloader?</string>

View File

@@ -4,3 +4,4 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
android.nonFinalResIds=false
org.gradle.caching=true
version=1.0.0