mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-10 05:16:16 +00:00
feat: Multiple downloader per APK
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,3 +4,4 @@ kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.caching=true
|
||||
version=1.0.0
|
||||
Reference in New Issue
Block a user