diff --git a/api/src/main/kotlin/app/revanced/manager/downloader/webview/API.kt b/api/src/main/kotlin/app/revanced/manager/downloader/webview/API.kt index 1f4f4313..4cf8fd3e 100644 --- a/api/src/main/kotlin/app/revanced/manager/downloader/webview/API.kt +++ b/api/src/main/kotlin/app/revanced/manager/downloader/webview/API.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt index e8421ee6..2a499e50 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderRepository.kt @@ -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()) - val downloaderStates = _downloaderStates.asStateFlow() - val loadedDownloaderFlow = downloaderStates.map { states -> - states.values.filterIsInstance().map { it.downloader } + private val _downloaderPackageStates = MutableStateFlow(emptyMap()) + val downloaderPackageStates = _downloaderPackageStates.asStateFlow() + val loadedDownloaderPackageFlow = downloaderPackageStates.map { states -> + states.values.filterIsInstance().flatMap { it.downloader } } - private val acknowledgedDownloader = prefs.acknowledgedDownloader + private val acknowledgedPackageDownloader = prefs.acknowledgedDownloader private val installedDownloaderPackageNames = MutableStateFlow(emptySet()) 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 { 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 diff --git a/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPackageState.kt b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPackageState.kt new file mode 100644 index 00000000..4f622e6f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPackageState.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.network.downloader + +sealed interface DownloaderPackageState { + data object Untrusted : DownloaderPackageState + + data class Loaded(val downloader: List) : DownloaderPackageState + + data class Failed(val throwable: Throwable) : DownloaderPackageState +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/DownloaderState.kt b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderState.kt deleted file mode 100644 index 6c35d7b0..00000000 --- a/app/src/main/java/app/revanced/manager/network/downloader/DownloaderState.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt index 4ebda0ab..71d2742b 100644 --- a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt +++ b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt @@ -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) ) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 07983e54..aa407602 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -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() diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 3a0a14a5..35890717 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -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 ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt index 1173b388..f2f8c94b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -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), diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt index a6f4bbad..ddd8736c 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt @@ -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 { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 4869909e..2ccc8acf 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cd18037..4750bf3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ You will not be able to update the previously installed apps from this source."< You are about to reset all patch options. You will have to reapply each option again. Resets all patch options Downloader - Trusted + Trusted%s Failed to load. Click for more details Untrusted Trust downloader? diff --git a/gradle.properties b/gradle.properties index 9985acf5..4cd8789a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,4 @@ kotlin.code.style=official android.nonTransitiveRClass=true android.nonFinalResIds=false org.gradle.caching=true +version=1.0.0 \ No newline at end of file