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

View File

@@ -182,7 +182,7 @@ class PatcherWorker(
is SelectedApp.Search -> { is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) { runStep(StepId.DownloadAPK, args.onEvent) {
downloaderRepository.loadedDownloaderFlow.first() downloaderRepository.loadedDownloaderPackageFlow.first()
.firstNotNullOfOrNull { downloader -> .firstNotNullOfOrNull { downloader ->
try { try {
val getScope = object : GetScope { val getScope = object : GetScope {
@@ -250,11 +250,21 @@ class PatcherWorker(
tag, tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() "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() Result.failure()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e) 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() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import android.R.attr.name
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -201,7 +202,7 @@ fun SelectedAppInfoScreen(
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed) is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource( is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader, 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 ?: app.data.downloaderPackageName
) )

View File

@@ -39,7 +39,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.network.downloader.DownloaderState import app.revanced.manager.network.downloader.DownloaderPackageState
import app.revanced.manager.ui.component.AppLabel import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.ConfirmDialog
@@ -102,98 +102,105 @@ fun DownloadsSettingsScreen(
item { item {
GroupHeader(stringResource(R.string.downloader)) GroupHeader(stringResource(R.string.downloader))
} }
downloaderStates.forEach { (packageName, state) ->
item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
fun dismiss() { if (downloaderStates.isNotEmpty()) {
showDialog = false downloaderStates.forEach { (packageName, state) ->
} item(key = packageName) {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
val packageInfo = fun dismiss() {
remember(packageName) { showDialog = false
viewModel.pm.getPackageInfo( }
packageName
)
} ?: return@item
if (showDialog) { val packageInfo =
val signature =
remember(packageName) { remember(packageName) {
val androidSignature = viewModel.pm.getPackageInfo(
viewModel.pm.getSignature(packageName) packageName
val hash = MessageDigest.getInstance("SHA-256") )
.digest(androidSignature.toByteArray()) } ?: return@item
hash.toHexString(format = HexFormat.UpperCase)
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) { when (state) {
is DownloaderState.Loaded -> R.string.downloader_state_trusted is DownloaderPackageState.Loaded -> TrustDialog(
is DownloaderState.Failed -> R.string.downloader_state_failed title = R.string.downloader_revoke_trust_dialog_title,
is DownloaderState.Untrusted -> R.string.downloader_state_untrusted 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!!) }
)
}
} }
} } else {
if (downloaderStates.isEmpty()) {
item { item {
Text( Text(
stringResource(R.string.no_downloader_installed), stringResource(R.string.no_downloader_installed),

View File

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

View File

@@ -73,7 +73,7 @@ class SelectedAppInfoViewModel(
private val pm: PM = get() private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get() val prefs: PreferencesManager = get()
val downloader = downloaderRepository.loadedDownloaderFlow val downloader = downloaderRepository.loadedDownloaderPackageFlow
val desiredVersion = input.app.version val desiredVersion = input.app.version
val packageName = input.app.packageName 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_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="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader">Downloader</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_failed">Failed to load. Click for more details</string>
<string name="downloader_state_untrusted">Untrusted</string> <string name="downloader_state_untrusted">Untrusted</string>
<string name="downloader_trust_dialog_title">Trust downloader?</string> <string name="downloader_trust_dialog_title">Trust downloader?</string>

View File

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