mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-18 23:16:50 +08:00
Compare commits
3 Commits
compose/fi
...
compose/fe
Author | SHA1 | Date | |
---|---|---|---|
ba1271b5b8 | |||
9a9d1ea775 | |||
690ab655f6 |
@ -27,7 +27,7 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||
resValue("string", "app_name", "ReVanced Manager (dev)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
@ -40,21 +40,12 @@ android {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
|
||||
val keystoreFile = file("keystore.jks")
|
||||
|
||||
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||
applicationIdSuffix = ".debug_signed"
|
||||
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||
if (project.hasProperty("signAsDebug")) {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
isPseudoLocalesEnabled = true
|
||||
} else {
|
||||
signingConfig = signingConfigs.create("release") {
|
||||
storeFile = keystoreFile
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "0L")
|
||||
@ -72,8 +63,7 @@ android {
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
resources.excludes.addAll(listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
@ -81,8 +71,7 @@ android {
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
)
|
||||
)
|
||||
))
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
@ -172,7 +161,7 @@ dependencies {
|
||||
implementation(libs.revanced.library)
|
||||
|
||||
// Downloader plugins
|
||||
implementation(libs.plugin.api)
|
||||
implementation(project(":downloader-plugin"))
|
||||
|
||||
// Native processes
|
||||
implementation(libs.kotlin.process)
|
||||
|
@ -277,10 +277,6 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.DeveloperOptions> {
|
||||
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Updates> {
|
||||
UpdatesSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
@ -316,6 +312,9 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
LicensesScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.DeveloperOptions> {
|
||||
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
@ -125,7 +124,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
@ -180,7 +179,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Exception] occurred in the remote process while patching.
|
||||
* An [Exception] occured in the remote process while patching.
|
||||
*
|
||||
* @param originalStackTrace The stack trace of the original [Exception].
|
||||
*/
|
||||
|
@ -1,10 +1,8 @@
|
||||
package app.revanced.manager.patcher.runtime.process
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityThread
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
@ -97,10 +95,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val longArrayClass = LongArray::class.java
|
||||
private val emptyLongArray = LongArray(0)
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Looper.prepare()
|
||||
@ -111,15 +105,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
val systemContext = ActivityThread.systemMain().systemContext as Context
|
||||
val appContext = systemContext.createPackageContext(managerPackageName, 0)
|
||||
|
||||
// Avoid annoying logs. See https://github.com/robolectric/robolectric/blob/ad0484c6b32c7d11176c711abeb3cb4a900f9258/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java#L376-L388
|
||||
Class.forName("android.app.AppCompatCallbacks").apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
getDeclaredMethod("install", longArrayClass, longArrayClass).also { it.isAccessible = true }(null, emptyLongArray, emptyLongArray)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getDeclaredMethod("install", longArrayClass).also { it.isAccessible = true }(null, emptyLongArray)
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
|
@ -81,41 +81,3 @@ fun AppTopBar(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppTopBar(
|
||||
title: @Composable () -> Unit,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
backIcon: @Composable (() -> Unit) = @Composable {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
|
||||
R.string.back
|
||||
)
|
||||
)
|
||||
},
|
||||
actions: @Composable (RowScope.() -> Unit) = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
applyContainerColor: Boolean = false
|
||||
) {
|
||||
val containerColor = if (applyContainerColor) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = title,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
if (onBackClick != null) {
|
||||
IconButton(onClick = onBackClick) {
|
||||
backIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = actions,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
@ -24,7 +23,6 @@ import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefaul
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -34,8 +32,6 @@ fun BundleInformationDialog(
|
||||
bundle: PatchBundleSource,
|
||||
onUpdate: () -> Unit,
|
||||
) {
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val hasNetwork = remember { networkInfo.isConnected() }
|
||||
val composableScope = rememberCoroutineScope()
|
||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||
val isLocal = bundle is LocalPatchBundle
|
||||
@ -85,7 +81,7 @@ fun BundleInformationDialog(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isLocal && hasNetwork) {
|
||||
if (!isLocal) {
|
||||
IconButton(onClick = onUpdate) {
|
||||
Icon(
|
||||
Icons.Outlined.Update,
|
||||
|
@ -21,25 +21,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisallowComposableCalls
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@ -59,11 +42,7 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.FloatInputDialog
|
||||
import app.revanced.manager.ui.component.IntInputDialog
|
||||
import app.revanced.manager.ui.component.LongInputDialog
|
||||
import app.revanced.manager.ui.component.*
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||
@ -623,10 +602,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
interactionSource = interactionSource,
|
||||
onLongClickLabel = stringResource(R.string.select),
|
||||
onLongClick = {
|
||||
if (!deleteMode) {
|
||||
deletionTargets.add(item.key)
|
||||
deleteMode = true
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (!deleteMode) {
|
||||
|
@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.CalendarToday
|
||||
import androidx.compose.material.icons.outlined.Campaign
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.Sell
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@ -34,18 +37,28 @@ fun Changelog(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NewReleases,
|
||||
imageVector = Icons.Outlined.Campaign,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
"${version.removePrefix("v")} ($publishDate)",
|
||||
version.removePrefix("v"),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Tag(
|
||||
Icons.Outlined.CalendarToday,
|
||||
publishDate
|
||||
)
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
markdown,
|
||||
|
@ -12,34 +12,34 @@ import kotlinx.coroutines.flow.map
|
||||
data class BundleInfo(
|
||||
val name: String,
|
||||
val uid: Int,
|
||||
val compatible: List<PatchInfo>,
|
||||
val incompatible: List<PatchInfo>,
|
||||
val supported: List<PatchInfo>,
|
||||
val unsupported: List<PatchInfo>,
|
||||
val universal: List<PatchInfo>
|
||||
) {
|
||||
val all = sequence {
|
||||
yieldAll(compatible)
|
||||
yieldAll(incompatible)
|
||||
yieldAll(supported)
|
||||
yieldAll(unsupported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
|
||||
val patchCount get() = compatible.size + incompatible.size + universal.size
|
||||
val patchCount get() = supported.size + unsupported.size + universal.size
|
||||
|
||||
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
|
||||
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
|
||||
all
|
||||
} else {
|
||||
sequence {
|
||||
yieldAll(compatible)
|
||||
yieldAll(supported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
}
|
||||
|
||||
companion object Extensions {
|
||||
inline fun Iterable<BundleInfo>.toPatchSelection(
|
||||
allowIncompatible: Boolean,
|
||||
allowUnsupported: Boolean,
|
||||
condition: (Int, PatchInfo) -> Boolean
|
||||
): PatchSelection = this.associate { bundle ->
|
||||
val patches =
|
||||
bundle.patchSequence(allowIncompatible)
|
||||
bundle.patchSequence(allowUnsupported)
|
||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||
patch.name.takeIf {
|
||||
condition(
|
||||
@ -60,8 +60,8 @@ data class BundleInfo(
|
||||
source.state.map { state ->
|
||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
||||
|
||||
val compatible = mutableListOf<PatchInfo>()
|
||||
val incompatible = mutableListOf<PatchInfo>()
|
||||
val supported = mutableListOf<PatchInfo>()
|
||||
val unsupported = mutableListOf<PatchInfo>()
|
||||
val universal = mutableListOf<PatchInfo>()
|
||||
|
||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
||||
@ -70,15 +70,15 @@ data class BundleInfo(
|
||||
it.supports(
|
||||
packageName,
|
||||
version
|
||||
) -> compatible
|
||||
) -> supported
|
||||
|
||||
else -> incompatible
|
||||
else -> unsupported
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
BundleInfo(source.getName(), source.uid, compatible, incompatible, universal)
|
||||
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -20,19 +19,13 @@ fun BundleListScreen(
|
||||
selectedSources: SnapshotStateList<PatchBundleSource>,
|
||||
bundlesSelectable: Boolean,
|
||||
) {
|
||||
val sortedSources = remember(sources) {
|
||||
sources.sortedByDescending { source ->
|
||||
source.state.value.patchBundleOrNull()?.patches?.size ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
items(
|
||||
sortedSources,
|
||||
sources,
|
||||
key = { it.uid }
|
||||
) { source ->
|
||||
BundleItem(
|
||||
|
@ -6,7 +6,6 @@ import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -265,6 +264,9 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
|
||||
false
|
||||
)
|
||||
Notifications(
|
||||
if (!Aapt.supportsDevice()) {
|
||||
{
|
||||
@ -276,23 +278,16 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
if (vm.showBatteryOptimizationsWarning) {
|
||||
if (showBatteryOptimizationsWarning) {
|
||||
{
|
||||
val batteryOptimizationsLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
vm.updateBatteryOptimizationsWarning()
|
||||
}
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Default.BatteryAlert,
|
||||
text = stringResource(R.string.battery_optimization_notification),
|
||||
onClick = {
|
||||
batteryOptimizationsLauncher.launch(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.fromParts("package", androidContext.packageName, null)
|
||||
)
|
||||
)
|
||||
androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${androidContext.packageName}")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -7,12 +7,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -20,14 +15,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.PostAdd
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@ -144,7 +132,7 @@ fun PatcherScreen(
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { exportApkLauncher.launch("${vm.packageName}_${vm.version}_revanced_patched.apk") },
|
||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||
enabled = patcherSucceeded == true
|
||||
) {
|
||||
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
|
||||
|
@ -76,8 +76,8 @@ import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionBut
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.patches.OptionItem
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.isScrollingUp
|
||||
@ -147,9 +147,9 @@ fun PatchesSelectorScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CheckedFilterChip(
|
||||
selected = vm.filter and SHOW_INCOMPATIBLE == 0,
|
||||
onClick = { vm.toggleFlag(SHOW_INCOMPATIBLE) },
|
||||
label = { Text(stringResource(R.string.this_version)) }
|
||||
selected = vm.filter and SHOW_UNSUPPORTED == 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||
label = { Text(stringResource(R.string.supported)) }
|
||||
)
|
||||
|
||||
CheckedFilterChip(
|
||||
@ -163,18 +163,18 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
|
||||
if (vm.compatibleVersions.isNotEmpty())
|
||||
IncompatiblePatchDialog(
|
||||
UnsupportedPatchDialog(
|
||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||
compatibleVersions = vm.compatibleVersions,
|
||||
supportedVersions = vm.compatibleVersions,
|
||||
onDismissRequest = vm::dismissDialogs
|
||||
)
|
||||
var showIncompatiblePatchesDialog by rememberSaveable {
|
||||
var showUnsupportedPatchesDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (showIncompatiblePatchesDialog)
|
||||
IncompatiblePatchesDialog(
|
||||
if (showUnsupportedPatchesDialog)
|
||||
UnsupportedPatchesDialog(
|
||||
appVersion = vm.appVersion ?: stringResource(R.string.any_version),
|
||||
onDismissRequest = { showIncompatiblePatchesDialog = false }
|
||||
onDismissRequest = { showUnsupportedPatchesDialog = false }
|
||||
)
|
||||
|
||||
vm.optionsDialog?.let { (bundle, patch) ->
|
||||
@ -204,7 +204,7 @@ fun PatchesSelectorScreen(
|
||||
uid: Int,
|
||||
patches: List<PatchInfo>,
|
||||
visible: Boolean,
|
||||
compatible: Boolean,
|
||||
supported: Boolean,
|
||||
header: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
if (patches.isNotEmpty() && visible) {
|
||||
@ -224,14 +224,14 @@ fun PatchesSelectorScreen(
|
||||
onOptionsDialog = {
|
||||
vm.optionsDialog = uid to patch
|
||||
},
|
||||
selected = compatible && vm.isSelected(
|
||||
selected = supported && vm.isSelected(
|
||||
uid,
|
||||
patch
|
||||
),
|
||||
onToggle = {
|
||||
when {
|
||||
// Open incompatible dialog if the patch is not supported
|
||||
!compatible -> vm.openIncompatibleDialog(patch)
|
||||
// Open unsupported dialog if the patch is not supported
|
||||
!supported -> vm.openUnsupportedDialog(patch)
|
||||
|
||||
// Show selection warning if enabled
|
||||
vm.selectionWarningEnabled -> showSelectionWarning = true
|
||||
@ -245,7 +245,7 @@ fun PatchesSelectorScreen(
|
||||
else -> vm.togglePatch(uid, patch)
|
||||
}
|
||||
},
|
||||
compatible = compatible
|
||||
supported = supported
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -321,15 +321,15 @@ fun PatchesSelectorScreen(
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible.searched(),
|
||||
patches = bundle.supported.searched(),
|
||||
visible = true,
|
||||
compatible = true
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal.searched(),
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@ -338,13 +338,13 @@ fun PatchesSelectorScreen(
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible.searched(),
|
||||
visible = vm.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = vm.allowIncompatiblePatches
|
||||
patches = bundle.unsupported.searched(),
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -427,15 +427,15 @@ fun PatchesSelectorScreen(
|
||||
) {
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible,
|
||||
patches = bundle.supported,
|
||||
visible = true,
|
||||
compatible = true
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal,
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@ -443,13 +443,13 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible,
|
||||
visible = vm.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = vm.allowIncompatiblePatches
|
||||
patches = bundle.unsupported,
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -506,24 +506,24 @@ private fun PatchItem(
|
||||
onOptionsDialog: () -> Unit,
|
||||
selected: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
compatible: Boolean = true
|
||||
supported: Boolean = true
|
||||
) = ListItem(
|
||||
modifier = Modifier
|
||||
.let { if (!compatible) it.alpha(0.5f) else it }
|
||||
.let { if (!supported) it.alpha(0.5f) else it }
|
||||
.clickable(onClick = onToggle)
|
||||
.fillMaxSize(),
|
||||
leadingContent = {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { onToggle() },
|
||||
enabled = compatible
|
||||
enabled = supported
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(patch.name) },
|
||||
supportingContent = patch.description?.let { { Text(it) } },
|
||||
trailingContent = {
|
||||
if (patch.options?.isNotEmpty() == true) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = supported) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
}
|
||||
@ -559,7 +559,7 @@ fun ListHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IncompatiblePatchesDialog(
|
||||
private fun UnsupportedPatchesDialog(
|
||||
appVersion: String,
|
||||
onDismissRequest: () -> Unit
|
||||
) = AlertDialog(
|
||||
@ -572,11 +572,11 @@ private fun IncompatiblePatchesDialog(
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.incompatible_patches)) },
|
||||
title = { Text(stringResource(R.string.unsupported_patches)) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.incompatible_patches_dialog,
|
||||
R.string.unsupported_patches_dialog,
|
||||
appVersion
|
||||
)
|
||||
)
|
||||
@ -584,9 +584,9 @@ private fun IncompatiblePatchesDialog(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun IncompatiblePatchDialog(
|
||||
private fun UnsupportedPatchDialog(
|
||||
appVersion: String,
|
||||
compatibleVersions: List<String>,
|
||||
supportedVersions: List<String>,
|
||||
onDismissRequest: () -> Unit
|
||||
) = AlertDialog(
|
||||
icon = {
|
||||
@ -598,13 +598,13 @@ private fun IncompatiblePatchDialog(
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.incompatible_patch)) },
|
||||
title = { Text(stringResource(R.string.unsupported_patch)) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.app_version_not_compatible,
|
||||
R.string.app_not_supported,
|
||||
appVersion,
|
||||
compatibleVersions.joinToString(", ")
|
||||
supportedVersions.joinToString(", ")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -14,7 +12,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
@ -35,7 +32,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
@ -44,7 +40,6 @@ import app.revanced.manager.ui.component.AppInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
@ -55,7 +50,6 @@ import app.revanced.manager.util.enabled
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -67,9 +61,6 @@ fun SelectedAppInfoScreen(
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
@ -217,35 +208,6 @@ fun SelectedAppInfoScreen(
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val needsInternet =
|
||||
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
||||
|
||||
when {
|
||||
!needsInternet -> {}
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_unavailable_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_metered_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,6 @@ private val settingsSections = listOf(
|
||||
R.string.advanced_description,
|
||||
Icons.Outlined.Tune
|
||||
) to Settings.Advanced,
|
||||
Triple(
|
||||
R.string.developer_options,
|
||||
R.string.developer_options_description,
|
||||
Icons.Outlined.Code
|
||||
) to Settings.DeveloperOptions,
|
||||
Triple(
|
||||
R.string.about,
|
||||
R.string.app_name,
|
||||
|
@ -5,17 +5,19 @@ import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Cancel
|
||||
import androidx.compose.material.icons.outlined.InstallMobile
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -26,15 +28,16 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.settings.Changelog
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||
@ -57,64 +60,13 @@ fun UpdateScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(stringResource(vm.state.title))
|
||||
|
||||
if (vm.state == State.DOWNLOADING) {
|
||||
Text(
|
||||
text = "${vm.downloadedSize.div(1000000)} MB / ${
|
||||
vm.totalSize.div(1000000)
|
||||
} MB (${vm.downloadProgress.times(100).toInt()}%)",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.update),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
val buttonConfig = when (vm.state) {
|
||||
State.CAN_DOWNLOAD -> Triple(
|
||||
{ vm.downloadUpdate() },
|
||||
R.string.download,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
|
||||
State.CAN_INSTALL -> Triple(
|
||||
{ vm.installUpdate() },
|
||||
R.string.install_app,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
buttonConfig?.let { (onClick, textRes, icon) ->
|
||||
HapticExtendedFloatingActionButton(
|
||||
onClick = onClick::invoke,
|
||||
icon = { Icon(icon, null) },
|
||||
text = { Text(stringResource(textRes)) }
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
) {
|
||||
if (vm.state == State.DOWNLOADING)
|
||||
LinearProgressIndicator(
|
||||
progress = { vm.downloadProgress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||
MeteredDownloadConfirmationDialog(
|
||||
onDismiss = { vm.showInternetCheckDialog = false },
|
||||
@ -124,14 +76,21 @@ fun UpdateScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(paddingValues)
|
||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
Header(
|
||||
vm.state,
|
||||
vm.releaseInfo,
|
||||
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize)
|
||||
)
|
||||
vm.releaseInfo?.let { changelog ->
|
||||
HorizontalDivider()
|
||||
Changelog(changelog)
|
||||
}
|
||||
}
|
||||
} ?: Spacer(modifier = Modifier.weight(1f))
|
||||
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,6 +123,58 @@ private fun MeteredDownloadConfirmationDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(state.title),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
if (state == State.CAN_DOWNLOAD) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.current_version,
|
||||
BuildConfig.VERSION_NAME
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
releaseInfo?.version?.let {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.new_version,
|
||||
it.replace("v", "")
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (state == State.DOWNLOADING) {
|
||||
LinearProgressIndicator(
|
||||
progress = { downloadData.downloadProgress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
"${downloadData.downloadedSize.div(1000000)} MB / ${
|
||||
downloadData.totalSize.div(
|
||||
1000000
|
||||
)
|
||||
} MB (${
|
||||
downloadData.downloadProgress.times(
|
||||
100
|
||||
).toInt()
|
||||
}%)",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
val scrollState = rememberScrollState()
|
||||
@ -195,3 +206,39 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(
|
||||
state: State,
|
||||
onDownloadClick: () -> Unit,
|
||||
onInstallClick: () -> Unit,
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
if (state.showCancel) {
|
||||
TextButton(
|
||||
onClick = onBackClick,
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (state == State.CAN_DOWNLOAD) {
|
||||
Button(onClick = onDownloadClick) {
|
||||
Text(text = stringResource(R.string.update))
|
||||
}
|
||||
} else if (state == State.CAN_INSTALL) {
|
||||
Button(
|
||||
onClick = onInstallClick
|
||||
) {
|
||||
Text(text = stringResource(R.string.install_app))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadData(
|
||||
val downloadProgress: Float,
|
||||
val downloadedSize: Long,
|
||||
val totalSize: Long
|
||||
)
|
@ -44,7 +44,6 @@ import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
||||
import app.revanced.manager.util.openUrl
|
||||
import app.revanced.manager.util.toast
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@ -117,14 +116,12 @@ fun AboutSettingsScreen(
|
||||
Triple(
|
||||
stringResource(R.string.contributors),
|
||||
stringResource(R.string.contributors_description),
|
||||
third = nav@{
|
||||
if (!viewModel.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@nav
|
||||
}
|
||||
|
||||
navigate(Settings.Contributors)
|
||||
}
|
||||
third = { navigate(Settings.Contributors) }
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.developer_options),
|
||||
stringResource(R.string.developer_options_description),
|
||||
third = { navigate(Settings.DeveloperOptions) }
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.opensource_licenses),
|
||||
|
@ -112,6 +112,20 @@ fun AdvancedSettingsScreen(
|
||||
}
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.useProcessRuntime,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime,
|
||||
description = R.string.process_runtime_description,
|
||||
)
|
||||
IntegerItem(
|
||||
preference = vm.prefs.patcherProcessMemoryLimit,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime_memory_limit,
|
||||
description = R.string.process_runtime_memory_limit_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.safeguards))
|
||||
SafeguardBooleanItem(
|
||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
||||
@ -142,20 +156,6 @@ fun AdvancedSettingsScreen(
|
||||
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.useProcessRuntime,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime,
|
||||
description = R.string.process_runtime_description,
|
||||
)
|
||||
IntegerItem(
|
||||
preference = vm.prefs.patcherProcessMemoryLimit,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime_memory_limit,
|
||||
description = R.string.process_runtime_memory_limit_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.debugging))
|
||||
val exportDebugLogsLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
||||
|
@ -97,14 +97,7 @@ fun ContributorScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
} ?: item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -10,10 +12,13 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@ -31,6 +36,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -59,6 +65,7 @@ fun DownloadsSettingsScreen(
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@ -129,15 +136,18 @@ fun DownloadsSettingsScreen(
|
||||
.digest(androidSignature.toByteArray())
|
||||
hash.toHexString(format = HexFormat.UpperCase)
|
||||
}
|
||||
val appName = packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
||||
?.toString()
|
||||
?: packageName
|
||||
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||
title = R.string.downloader_plugin_revoke_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
R.string.downloader_plugin_revoke_trust_dialog_body,
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.revokePluginTrust(packageName)
|
||||
@ -152,13 +162,14 @@ fun DownloadsSettingsScreen(
|
||||
onDismiss = ::dismiss
|
||||
)
|
||||
|
||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||
is DownloaderPluginState.Untrusted ->
|
||||
TrustDialog(
|
||||
title = R.string.downloader_plugin_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
R.string.downloader_plugin_trust_dialog_body
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.trustPlugin(packageName)
|
||||
@ -233,6 +244,8 @@ fun DownloadsSettingsScreen(
|
||||
private fun TrustDialog(
|
||||
@StringRes title: Int,
|
||||
body: String,
|
||||
pluginName: String,
|
||||
signature: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
@ -249,6 +262,39 @@ private fun TrustDialog(
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(title)) },
|
||||
text = { Text(body) }
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(body)
|
||||
Card(
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_plugin,
|
||||
pluginName
|
||||
),
|
||||
)
|
||||
OutlinedCard(
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_signature,
|
||||
signature.chunked(2).joinToString(" ")
|
||||
), modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@ -19,7 +18,6 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@ -31,7 +29,6 @@ fun UpdatesSettingsScreen(
|
||||
onUpdateClick: () -> Unit,
|
||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
@ -53,10 +50,6 @@ fun UpdatesSettingsScreen(
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@launch
|
||||
}
|
||||
if (vm.checkForUpdates()) onUpdateClick()
|
||||
}
|
||||
},
|
||||
@ -65,13 +58,7 @@ fun UpdatesSettingsScreen(
|
||||
)
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
if (!vm.isConnected) {
|
||||
context.toast(context.getString(R.string.no_network_toast))
|
||||
return@clickable
|
||||
}
|
||||
onChangelogClick()
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onChangelogClick),
|
||||
headlineContent = stringResource(R.string.changelog),
|
||||
supportingContent = stringResource(
|
||||
R.string.changelog_description
|
||||
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedDonationLink
|
||||
import app.revanced.manager.network.dto.ReVancedSocial
|
||||
@ -24,24 +23,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AboutViewModel(
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val network: NetworkInfo,
|
||||
) : ViewModel() {
|
||||
class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() {
|
||||
var socials by mutableStateOf(emptyList<ReVancedSocial>())
|
||||
private set
|
||||
var contact by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var donate by mutableStateOf<String?>(null)
|
||||
private set
|
||||
val isConnected: Boolean
|
||||
get() = network.isConnected()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (!isConnected) {
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
reVancedAPI.getInfo("https://api.revanced.app").getOrNull()
|
||||
}?.let {
|
||||
|
@ -24,7 +24,9 @@ import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -56,13 +58,19 @@ class DashboardViewModel(
|
||||
|
||||
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||
private set
|
||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||
private set
|
||||
val showBatteryOptimizationsWarningFlow = flow {
|
||||
while (true) {
|
||||
// There is no callback for this, so we have to poll it.
|
||||
val result = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
emit(result)
|
||||
if (!result) return@flow
|
||||
delay(500L)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
checkForManagerUpdates()
|
||||
updateBatteryOptimizationsWarning()
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,10 +90,6 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBatteryOptimizationsWarning() {
|
||||
showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
}
|
||||
|
||||
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
|
||||
viewModelScope.launch {
|
||||
prefs.showManagerUpdateDialogOnLaunch.update(value)
|
||||
|
@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.resetListItemColorsCached
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GeneralSettingsViewModel(
|
||||
@ -12,6 +11,5 @@ class GeneralSettingsViewModel(
|
||||
) : ViewModel() {
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||
prefs.theme.update(theme)
|
||||
resetListItemColorsCached()
|
||||
}
|
||||
}
|
@ -83,9 +83,7 @@ class PatcherViewModel(
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
private val selectedApp = input.selectedApp
|
||||
val packageName = selectedApp.packageName
|
||||
val version = selectedApp.version
|
||||
val packageName = input.selectedApp.packageName
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
|
@ -3,11 +3,11 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
@ -30,20 +30,15 @@ import app.revanced.manager.util.saver.persistentMapSaver
|
||||
import app.revanced.manager.util.saver.persistentSetSaver
|
||||
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.PersistentSet
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
||||
@ -213,8 +208,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
compatibleVersions.clear()
|
||||
}
|
||||
|
||||
fun openIncompatibleDialog(incompatiblePatch: PatchInfo) {
|
||||
compatibleVersions.addAll(incompatiblePatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
|
||||
fun openUnsupportedDialog(unsupportedPatch: PatchInfo) {
|
||||
compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty())
|
||||
}
|
||||
|
||||
fun toggleFlag(flag: Int) {
|
||||
@ -222,7 +217,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHOW_INCOMPATIBLE = 1 // 2^0
|
||||
const val SHOW_UNSUPPORTED = 1 // 2^0
|
||||
const val SHOW_UNIVERSAL = 2 // 2^1
|
||||
|
||||
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||
|
@ -30,13 +30,14 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
@ -274,25 +275,25 @@ class SelectedAppInfoViewModel(
|
||||
)
|
||||
|
||||
suspend fun getPatcherParams(): Patcher.ViewModelParams {
|
||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = bundleInfoFlow.first()
|
||||
return Patcher.ViewModelParams(
|
||||
selectedApp,
|
||||
getPatches(bundles, allowIncompatible),
|
||||
getPatches(bundles, allowUnsupported),
|
||||
getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||
|
||||
fun getPatches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
||||
selectionState.patches(bundles, allowIncompatible)
|
||||
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
selectionState.patches(bundles, allowUnsupported)
|
||||
|
||||
fun getCustomPatches(
|
||||
bundles: List<BundleInfo>,
|
||||
allowIncompatible: Boolean
|
||||
allowUnsupported: Boolean
|
||||
): PatchSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||
|
||||
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
|
||||
val bundles = bundleInfoFlow.first()
|
||||
@ -342,13 +343,13 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
|
||||
private sealed interface SelectionState : Parcelable {
|
||||
fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean): PatchSelection
|
||||
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection
|
||||
|
||||
@Parcelize
|
||||
data class Customized(val patchSelection: PatchSelection) : SelectionState {
|
||||
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
||||
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
bundles.toPatchSelection(
|
||||
allowIncompatible
|
||||
allowUnsupported
|
||||
) { uid, patch ->
|
||||
patchSelection[uid]?.contains(patch.name) ?: false
|
||||
}
|
||||
@ -356,8 +357,8 @@ private sealed interface SelectionState : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data object Default : SelectionState {
|
||||
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
||||
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
|
||||
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -43,9 +42,9 @@ class UpdateViewModel(
|
||||
private val networkInfo: NetworkInfo by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
||||
var downloadedSize by mutableLongStateOf(0L)
|
||||
var downloadedSize by mutableStateOf(0L)
|
||||
private set
|
||||
var totalSize by mutableLongStateOf(0L)
|
||||
var totalSize by mutableStateOf(0L)
|
||||
private set
|
||||
val downloadProgress by derivedStateOf {
|
||||
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
|
||||
@ -90,7 +89,7 @@ class UpdateViewModel(
|
||||
totalSize = contentLength
|
||||
}
|
||||
}
|
||||
installUpdate()
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,10 +140,10 @@ class UpdateViewModel(
|
||||
location.delete()
|
||||
}
|
||||
|
||||
enum class State(@StringRes val title: Int) {
|
||||
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update),
|
||||
CAN_INSTALL(R.string.ready_to_install_update),
|
||||
DOWNLOADING(R.string.downloading_manager_update, true),
|
||||
CAN_INSTALL(R.string.ready_to_install_update, true),
|
||||
INSTALLING(R.string.installing_manager_update),
|
||||
FAILED(R.string.install_update_manager_failed),
|
||||
SUCCESS(R.string.update_completed)
|
||||
|
@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.toast
|
||||
@ -13,14 +12,10 @@ class UpdatesSettingsViewModel(
|
||||
prefs: PreferencesManager,
|
||||
private val app: Application,
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val network: NetworkInfo,
|
||||
) : ViewModel() {
|
||||
val managerAutoUpdates = prefs.managerAutoUpdates
|
||||
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
|
||||
|
||||
val isConnected: Boolean
|
||||
get() = network.isConnected()
|
||||
|
||||
suspend fun checkForUpdates(): Boolean {
|
||||
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
|
||||
app.toast(app.getString(R.string.update_check))
|
||||
|
@ -180,10 +180,6 @@ fun LocalDateTime.relativeTime(context: Context): String {
|
||||
|
||||
private var transparentListItemColorsCached: ListItemColors? = null
|
||||
|
||||
fun resetListItemColorsCached() {
|
||||
transparentListItemColorsCached = null
|
||||
}
|
||||
|
||||
/**
|
||||
* The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent].
|
||||
*/
|
||||
|
@ -38,8 +38,6 @@
|
||||
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
||||
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
|
||||
|
||||
<string name="no_network_toast">No internet connection available</string>
|
||||
|
||||
<string name="selected_app_meta_any_version">Any available version</string>
|
||||
<string name="app_source_dialog_title">Select source</string>
|
||||
<string name="app_source_dialog_option_auto">Auto</string>
|
||||
@ -53,9 +51,6 @@
|
||||
<string name="patch_selector_item_description">%d patches selected</string>
|
||||
<string name="no_patches_selected">No patches selected</string>
|
||||
|
||||
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
|
||||
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||
|
||||
<string name="apk_source_selector_item">Change source</string>
|
||||
<string name="apk_source_auto">Current: All downloaders</string>
|
||||
<string name="apk_source_downloader">Current: %s</string>
|
||||
@ -92,7 +87,7 @@
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to supported app versions</string>
|
||||
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||
@ -142,7 +137,10 @@
|
||||
<string name="downloader_plugin_state_untrusted">Untrusted</string>
|
||||
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
|
||||
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">By continuing you\'ve agreed to running external plugins.\n\nDo not allow any suspicious plugin(s) to be installed as they can run arbitrary code.</string>
|
||||
<string name="downloader_plugin_revoke_trust_dialog_body">By revoking trust, this plugin will be disabled and prevented from running.</string>
|
||||
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
|
||||
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
@ -216,7 +214,7 @@
|
||||
<string name="no_patched_apps_found">No patched apps found</string>
|
||||
<string name="tap_on_patches">Tap on the patches to get more information about them</string>
|
||||
<string name="bundles_selected">%s selected</string>
|
||||
<string name="incompatible_patches">Incompatible patches</string>
|
||||
<string name="unsupported_patches">Incompatible patches</string>
|
||||
<string name="universal_patches">Universal patches</string>
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
@ -225,12 +223,13 @@
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string>
|
||||
<string name="this_version">This version</string>
|
||||
<string name="supported">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="unsupported">Unsupported</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string>
|
||||
<string name="app_not_supported">This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s.</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
||||
<string name="failed_to_load_apk">Failed to load APK</string>
|
||||
@ -404,7 +403,7 @@
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is compatible by this device and try again.</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</string>
|
||||
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
|
||||
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
|
||||
<string name="installation_timeout_description">The installation took too long. Try again?</string>
|
||||
@ -417,8 +416,8 @@
|
||||
<string name="add_patch_bundle">Add patch bundle</string>
|
||||
<string name="bundle_url">Bundle URL</string>
|
||||
<string name="auto_update">Auto update</string>
|
||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="incompatible_patch">Incompatible patch</string>
|
||||
<string name="unsupported_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="unsupported_patch">Unsupported patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
|
||||
|
@ -7,4 +7,10 @@ plugins {
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
alias(libs.plugins.about.libraries) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
ignoredProjects.addAll(listOf("app", "example-downloader-plugin"))
|
||||
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
|
||||
}
|
1
downloader-plugin/.gitignore
vendored
Normal file
1
downloader-plugin/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
171
downloader-plugin/api/downloader-plugin.api
Normal file
171
downloader-plugin/api/downloader-plugin.api
Normal file
@ -0,0 +1,171 @@
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/ConstantsKt {
|
||||
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable {
|
||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
|
||||
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun component2 ()Ljava/util/Map;
|
||||
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public final fun describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getHeaders ()Ljava/util/Map;
|
||||
public final fun getUrl ()Ljava/lang/String;
|
||||
public fun hashCode ()I
|
||||
public final fun toDownloadResult ()Lkotlin/Pair;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Downloader {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
|
||||
public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public final fun download (Lkotlin/jvm/functions/Function3;)V
|
||||
public final fun get (Lkotlin/jvm/functions/Function4;)V
|
||||
public fun getHostPackageName ()Ljava/lang/String;
|
||||
public fun getPluginPackageName ()Ljava/lang/String;
|
||||
public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
|
||||
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope {
|
||||
public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
|
||||
public static final field CREATOR Landroid/os/Parcelable$Creator;
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
|
||||
public final fun component1 ()Ljava/lang/String;
|
||||
public final fun component2 ()Ljava/lang/String;
|
||||
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public final fun describeContents ()I
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getName ()Ljava/lang/String;
|
||||
public final fun getVersion ()Ljava/lang/String;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
public final fun writeToParcel (Landroid/os/Parcel;I)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun getHostPackageName ()Ljava/lang/String;
|
||||
public abstract fun getPluginPackageName ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
|
||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
|
||||
public final fun getIntent ()Landroid/content/Intent;
|
||||
public final fun getResultCode ()I
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/APIKt {
|
||||
public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
|
||||
public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public fun finish ()V
|
||||
public fun load (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
|
||||
public fun pageLoad (Ljava/lang/String;)V
|
||||
public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V
|
||||
}
|
||||
|
||||
public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents {
|
||||
public fun <init> ()V
|
||||
public fun asBinder ()Landroid/os/IBinder;
|
||||
public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents;
|
||||
public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator {
|
||||
public fun <init> ()V
|
||||
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
|
||||
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters;
|
||||
public synthetic fun newArray (I)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope {
|
||||
public final fun download (Lkotlin/jvm/functions/Function5;)V
|
||||
public fun getHostPackageName ()Ljava/lang/String;
|
||||
public fun getPluginPackageName ()Ljava/lang/String;
|
||||
public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V
|
||||
}
|
||||
|
61
downloader-plugin/build.gradle.kts
Normal file
61
downloader-plugin/build.gradle.kts
Normal file
@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.manager.plugin.downloader"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.activity.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.appcompat)
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
publications {
|
||||
create<MavenPublication>("release") {
|
||||
groupId = "app.revanced"
|
||||
artifactId = "manager-downloader-plugin"
|
||||
version = "1.0"
|
||||
|
||||
afterEvaluate {
|
||||
from(components["release"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
downloader-plugin/consumer-rules.pro
Normal file
0
downloader-plugin/consumer-rules.pro
Normal file
21
downloader-plugin/proguard-rules.pro
vendored
Normal file
21
downloader-plugin/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
3
downloader-plugin/src/main/AndroidManifest.xml
Normal file
3
downloader-plugin/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
@ -0,0 +1,8 @@
|
||||
// IWebView.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||
oneway interface IWebView {
|
||||
void load(String url);
|
||||
void finish();
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// IWebViewEvents.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
import app.revanced.manager.plugin.downloader.webview.IWebView;
|
||||
|
||||
@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi")
|
||||
oneway interface IWebViewEvents {
|
||||
void ready(IWebView iface);
|
||||
void pageLoad(String url);
|
||||
void download(String url, String mimetype, String userAgent);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
/**
|
||||
* The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission.
|
||||
* Plugin UI activities and internal services can be protected using this permission.
|
||||
*/
|
||||
const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST"
|
@ -0,0 +1,165 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.app.Activity
|
||||
import android.os.Parcelable
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@RequiresOptIn(
|
||||
level = RequiresOptIn.Level.ERROR,
|
||||
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
|
||||
)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class PluginHostApi
|
||||
|
||||
/**
|
||||
* The base interface for all DSL scopes.
|
||||
*/
|
||||
interface Scope {
|
||||
/**
|
||||
* The package name of ReVanced Manager.
|
||||
*/
|
||||
val hostPackageName: String
|
||||
|
||||
/**
|
||||
* The package name of the plugin.
|
||||
*/
|
||||
val pluginPackageName: String
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope of [DownloaderScope.get].
|
||||
*/
|
||||
interface GetScope : Scope {
|
||||
/**
|
||||
* Ask the user to perform some required interaction in the activity specified by the provided [Intent].
|
||||
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
|
||||
*
|
||||
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
|
||||
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
|
||||
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
|
||||
*/
|
||||
suspend fun requestStartActivity(intent: Intent): Intent?
|
||||
}
|
||||
|
||||
interface BaseDownloadScope : Scope
|
||||
|
||||
/**
|
||||
* The scope for [DownloaderScope.download].
|
||||
*/
|
||||
interface InputDownloadScope : BaseDownloadScope
|
||||
|
||||
typealias Size = Long
|
||||
typealias DownloadResult = Pair<InputStream, Size?>
|
||||
|
||||
typealias Version = String
|
||||
typealias GetResult<T> = Pair<T, Version?>
|
||||
|
||||
class DownloaderScope<T : Parcelable> internal constructor(
|
||||
private val scopeImpl: Scope,
|
||||
internal val context: Context
|
||||
) : Scope by scopeImpl {
|
||||
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
|
||||
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
|
||||
internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null
|
||||
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
|
||||
private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {}
|
||||
|
||||
/**
|
||||
* Define the download block of the plugin.
|
||||
*/
|
||||
fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) {
|
||||
download = { app, outputStream ->
|
||||
val (inputStream, size) = inputDownloadScopeImpl.block(app)
|
||||
|
||||
inputStream.use {
|
||||
if (size != null) reportSize(size)
|
||||
it.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the get block of the plugin.
|
||||
* The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null.
|
||||
*/
|
||||
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
|
||||
get = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
|
||||
*/
|
||||
suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R {
|
||||
var onBind: ((IBinder) -> Unit)? = null
|
||||
val serviceConn = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
|
||||
onBind!!(service!!)
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
}
|
||||
|
||||
return try {
|
||||
val binder = withTimeout(10000L) {
|
||||
suspendCoroutine { continuation ->
|
||||
onBind = continuation::resume
|
||||
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
block(binder)
|
||||
} finally {
|
||||
onBind = null
|
||||
context.unbindService(serviceConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
|
||||
@PluginHostApi
|
||||
fun build(scopeImpl: Scope, context: Context) =
|
||||
with(DownloaderScope<T>(scopeImpl, context)) {
|
||||
block()
|
||||
|
||||
Downloader(
|
||||
download = download!!,
|
||||
get = get!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Downloader<T : Parcelable> internal constructor(
|
||||
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
|
||||
@property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit
|
||||
)
|
||||
|
||||
/**
|
||||
* Define a downloader plugin.
|
||||
*/
|
||||
fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
|
||||
|
||||
/**
|
||||
* @see GetScope.requestStartActivity
|
||||
*/
|
||||
sealed class UserInteractionException(message: String) : Exception(message) {
|
||||
class RequestDenied @PluginHostApi constructor() :
|
||||
UserInteractionException("Request denied by user")
|
||||
|
||||
sealed class Activity(message: String) : UserInteractionException(message) {
|
||||
class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled")
|
||||
|
||||
/**
|
||||
* @param resultCode The result code of the activity.
|
||||
* @param intent The [Intent] of the activity.
|
||||
*/
|
||||
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
|
||||
Activity("Unexpected activity result code: $resultCode")
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* The scope of the [OutputStream] version of [DownloaderScope.download].
|
||||
*/
|
||||
interface OutputDownloadScope : BaseDownloadScope {
|
||||
suspend fun reportSize(size: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacement for [DownloaderScope.download] that uses [OutputStream].
|
||||
* The provided [OutputStream] does not need to be closed manually.
|
||||
*/
|
||||
fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) {
|
||||
download = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
|
||||
* @see [GetScope.requestStartActivity]
|
||||
*/
|
||||
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
|
||||
requestStartActivity(
|
||||
Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
|
||||
)
|
||||
|
||||
/**
|
||||
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
|
||||
* @see [DownloaderScope.useService]
|
||||
*/
|
||||
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService(
|
||||
noinline block: suspend (IBinder) -> R
|
||||
) = useService(
|
||||
Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
|
||||
)
|
@ -0,0 +1,39 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* A simple parcelable data class for storing a package name and version.
|
||||
* This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function.
|
||||
*
|
||||
* @param name The package name.
|
||||
* @param version The version.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Package(val name: String, val version: String) : Parcelable
|
||||
|
||||
/**
|
||||
* A data class for storing a download URL.
|
||||
*
|
||||
* @param url The download URL.
|
||||
* @param headers The headers to use for the request.
|
||||
*/
|
||||
@Parcelize
|
||||
data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable {
|
||||
/**
|
||||
* Converts this into a [DownloadResult].
|
||||
*/
|
||||
fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
|
||||
useCaches = false
|
||||
allowUserInteraction = false
|
||||
headers.forEach(::setRequestProperty)
|
||||
|
||||
connectTimeout = 10_000
|
||||
connect()
|
||||
|
||||
inputStream to getHeaderField("Content-Length").toLong()
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.content.Intent
|
||||
import app.revanced.manager.plugin.downloader.DownloadUrl
|
||||
import app.revanced.manager.plugin.downloader.DownloaderScope
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.Scope
|
||||
import app.revanced.manager.plugin.downloader.Downloader
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
typealias InitialUrl = String
|
||||
typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
|
||||
typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
|
||||
|
||||
interface WebViewCallbackScope<T> : Scope {
|
||||
/**
|
||||
* Finishes the activity and returns the [result].
|
||||
*/
|
||||
suspend fun finish(result: T)
|
||||
|
||||
/**
|
||||
* Tells the WebView to load the specified [url].
|
||||
*/
|
||||
suspend fun load(url: String)
|
||||
}
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class WebViewScope<T> internal constructor(
|
||||
coroutineScope: CoroutineScope,
|
||||
private val scopeImpl: Scope,
|
||||
setResult: (T) -> Unit
|
||||
) : Scope by scopeImpl {
|
||||
private var onPageLoadCallback: PageLoadCallback<T> = {}
|
||||
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||
private lateinit var webView: IWebView
|
||||
internal lateinit var initialUrl: String
|
||||
|
||||
internal val binder = object : IWebViewEvents.Stub() {
|
||||
override fun ready(iface: IWebView?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
webView = iface!!.also {
|
||||
it.load(initialUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageLoad(url: String?) {
|
||||
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
|
||||
}
|
||||
|
||||
override fun download(url: String?, mimetype: String?, userAgent: String?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
onDownloadCallback(
|
||||
callbackScope,
|
||||
url!!,
|
||||
mimetype!!,
|
||||
userAgent!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
|
||||
override suspend fun finish(result: T) {
|
||||
setResult(result)
|
||||
// Tell the WebViewActivity to finish
|
||||
webView.let { withContext(Dispatchers.IO) { it.finish() } }
|
||||
}
|
||||
|
||||
override suspend fun load(url: String) {
|
||||
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the WebView attempts to download a file to disk.
|
||||
*/
|
||||
fun download(block: DownloadCallback<T>) {
|
||||
onDownloadCallback = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the WebView finishes loading a page.
|
||||
*/
|
||||
fun pageLoad(block: PageLoadCallback<T>) {
|
||||
onPageLoadCallback = block
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
private value class Container<U>(val value: U)
|
||||
|
||||
/**
|
||||
* Run a [android.webkit.WebView] Activity controlled by the provided code block.
|
||||
* The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish].
|
||||
* The [block] defines the event handlers and returns the initial URL.
|
||||
*
|
||||
* @param title The string displayed in the action bar.
|
||||
* @param block The control block.
|
||||
*/
|
||||
@OptIn(PluginHostApi::class)
|
||||
suspend fun <T> GetScope.runWebView(
|
||||
title: String,
|
||||
block: suspend WebViewScope<T>.() -> InitialUrl
|
||||
) = supervisorScope {
|
||||
var result by Delegates.notNull<Container<T>>()
|
||||
|
||||
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
|
||||
scope.initialUrl = scope.block()
|
||||
|
||||
// Start the webview activity and wait until it finishes.
|
||||
requestStartActivity(Intent().apply {
|
||||
putExtra(
|
||||
WebViewActivity.KEY,
|
||||
WebViewActivity.Parameters(title, scope.binder)
|
||||
)
|
||||
setClassName(
|
||||
hostPackageName,
|
||||
WebViewActivity::class.qualifiedName!!
|
||||
)
|
||||
})
|
||||
|
||||
// Return the result and cancel any leftover coroutines.
|
||||
coroutineContext.cancelChildren()
|
||||
result.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView].
|
||||
* Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get].
|
||||
*
|
||||
* @see runWebView
|
||||
*/
|
||||
fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) =
|
||||
Downloader<DownloadUrl> {
|
||||
val label = context.applicationInfo.loadLabel(
|
||||
context.packageManager
|
||||
).toString()
|
||||
|
||||
get { packageName, version ->
|
||||
class ReturnNull : Exception()
|
||||
|
||||
try {
|
||||
runWebView(label) {
|
||||
download { url, _, userAgent ->
|
||||
finish(
|
||||
DownloadUrl(
|
||||
url,
|
||||
mapOf("User-Agent" to userAgent)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
block(this@runWebView, packageName, version) ?: throw ReturnNull()
|
||||
} to version
|
||||
} catch (_: ReturnNull) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
download {
|
||||
it.toDownloadResult()
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.R
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
@PluginHostApi
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val vm by viewModels<WebViewModel>()
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_webview)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
val webView = findViewById<WebView>(R.id.webview)
|
||||
onBackPressedDispatcher.addCallback {
|
||||
if (webView.canGoBack()) webView.goBack()
|
||||
else cancelActivity()
|
||||
}
|
||||
|
||||
val params = intent.getParcelableExtra<Parameters>(KEY)!!
|
||||
actionBar?.apply {
|
||||
title = params.title
|
||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val events = IWebViewEvents.Stub.asInterface(params.events)!!
|
||||
vm.setup(events)
|
||||
|
||||
webView.apply {
|
||||
settings.apply {
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
allowContentAccess = false
|
||||
domStorageEnabled = true
|
||||
javaScriptEnabled = true
|
||||
}
|
||||
|
||||
webViewClient = vm.webViewClient
|
||||
setDownloadListener { url, userAgent, _, mimetype, _ ->
|
||||
vm.onDownload(url, mimetype, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
vm.commands.collect {
|
||||
when (it) {
|
||||
is WebViewModel.Command.Finish -> {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelActivity() {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
cancelActivity()
|
||||
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
@Parcelize
|
||||
internal class Parameters(
|
||||
val title: String, val events: IBinder
|
||||
) : Parcelable
|
||||
|
||||
internal companion object {
|
||||
const val KEY = "params"
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
internal class WebViewModel : ViewModel() {
|
||||
init {
|
||||
CookieManager.getInstance().apply {
|
||||
removeAllCookies(null)
|
||||
setAcceptCookie(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val commandChannel = Channel<Command>()
|
||||
val commands = commandChannel.receiveAsFlow()
|
||||
|
||||
private var eventBinder: IWebViewEvents? = null
|
||||
private val ctrlBinder = object : IWebView.Stub() {
|
||||
override fun load(url: String?) {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Load(url!!))
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Finish)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
eventBinder!!.pageLoad(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDownload(url: String, mimeType: String, userAgent: String) {
|
||||
eventBinder!!.download(url, mimeType, userAgent)
|
||||
}
|
||||
|
||||
fun setup(binder: IWebViewEvents) {
|
||||
if (eventBinder != null) return
|
||||
eventBinder = binder
|
||||
binder.ready(ctrlBinder)
|
||||
}
|
||||
|
||||
sealed interface Command {
|
||||
data class Load(val url: String) : Command
|
||||
data object Finish : Command
|
||||
}
|
||||
}
|
11
downloader-plugin/src/main/res/layout/activity_webview.xml
Normal file
11
downloader-plugin/src/main/res/layout/activity_webview.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/main">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
1
downloader-plugin/src/main/res/values/strings.xml
Normal file
1
downloader-plugin/src/main/res/values/strings.xml
Normal file
@ -0,0 +1 @@
|
||||
<resources></resources>
|
7
downloader-plugin/src/main/res/values/themes.xml
Normal file
7
downloader-plugin/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
|
||||
<item name="android:windowActionBar">true</item>
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
1
example-downloader-plugin/.gitignore
vendored
Normal file
1
example-downloader-plugin/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
53
example-downloader-plugin/build.gradle.kts
Normal file
53
example-downloader-plugin/build.gradle.kts
Normal file
@ -0,0 +1,53 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
val packageName = "app.revanced.manager.plugin.downloader.example"
|
||||
|
||||
namespace = packageName
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = packageName
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
if (project.hasProperty("signAsDebug")) {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures.compose = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.activity.compose)
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.material3)
|
||||
|
||||
compileOnly(project(":downloader-plugin"))
|
||||
}
|
21
example-downloader-plugin/proguard-rules.pro
vendored
Normal file
21
example-downloader-plugin/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
23
example-downloader-plugin/src/main/AndroidManifest.xml
Normal file
23
example-downloader-plugin/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="app.revanced.manager.plugin.downloader" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
android:name=".InteractionActivity"
|
||||
android:exported="true"
|
||||
android:permission="app.revanced.manager.permission.PLUGIN_HOST"
|
||||
android:theme="@android:style/Theme.DeviceDefault" />
|
||||
|
||||
<meta-data
|
||||
android:name="app.revanced.manager.plugin.downloader.class"
|
||||
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,69 @@
|
||||
@file:Suppress("Unused")
|
||||
|
||||
package app.revanced.manager.plugin.downloader.example
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.plugin.downloader.Downloader
|
||||
import app.revanced.manager.plugin.downloader.requestStartActivity
|
||||
import app.revanced.manager.plugin.downloader.webview.WebViewDownloader
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlin.io.path.*
|
||||
|
||||
val apkMirrorDownloader = WebViewDownloader { packageName, version ->
|
||||
with(Uri.Builder()) {
|
||||
scheme("https")
|
||||
authority("www.apkmirror.com")
|
||||
mapOf(
|
||||
"post_type" to "app_release",
|
||||
"searchtype" to "apk",
|
||||
"s" to (version?.let { "$packageName $it" } ?: packageName),
|
||||
"bundles%5B%5D" to "apk_files" // bundles[]
|
||||
).forEach { (key, value) ->
|
||||
appendQueryParameter(key, value)
|
||||
}
|
||||
|
||||
build().toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class InstalledApp(val path: String) : Parcelable
|
||||
|
||||
private val application by lazy {
|
||||
// Don't do this in a real plugin.
|
||||
val clazz = Class.forName("android.app.ActivityThread")
|
||||
val activityThread = clazz.getMethod("currentActivityThread")(null)
|
||||
clazz.getMethod("getApplication")(activityThread) as Application
|
||||
}
|
||||
|
||||
val installedAppDownloader = Downloader<InstalledApp> {
|
||||
val pm = application.packageManager
|
||||
|
||||
get { packageName, version ->
|
||||
val packageInfo = try {
|
||||
pm.getPackageInfo(packageName, 0)
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
return@get null
|
||||
}
|
||||
if (version != null && packageInfo.versionName != version) return@get null
|
||||
|
||||
requestStartActivity<InteractionActivity>()
|
||||
|
||||
InstalledApp(packageInfo.applicationInfo!!.sourceDir) to packageInfo.versionName
|
||||
}
|
||||
|
||||
|
||||
download { app ->
|
||||
with(Path(app.path)) { inputStream() to fileSize() }
|
||||
}
|
||||
|
||||
/*
|
||||
download { app, outputStream ->
|
||||
val path = Path(app.path)
|
||||
reportSize(path.fileSize())
|
||||
Files.copy(path, outputStream)
|
||||
}*/
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package app.revanced.manager.plugin.downloader.example
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
class InteractionActivity : ComponentActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()
|
||||
|
||||
MaterialTheme(colorScheme) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("User interaction example") }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
Text("This is an example interaction.")
|
||||
Row {
|
||||
TextButton(
|
||||
onClick = {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
) {
|
||||
Text("Continue")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Example Downloader Plugin</string>
|
||||
</resources>
|
@ -19,7 +19,6 @@ datetime = "0.6.1"
|
||||
room-version = "2.6.1"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
plugin-api = "1.0.0"
|
||||
koin = "3.5.3"
|
||||
ktor = "2.3.9"
|
||||
markdown-renderer = "0.30.0"
|
||||
@ -28,6 +27,7 @@ kotlin = "2.1.10"
|
||||
android-gradle-plugin = "8.8.0"
|
||||
dev-tools-gradle-plugin = "2.1.10-1.0.29"
|
||||
about-libraries-gradle-plugin = "11.5.0"
|
||||
binary-compatibility-validator = "0.17.0"
|
||||
coil = "2.7.0"
|
||||
app-icon-loader-coil = "1.5.0"
|
||||
skrapeit = "1.2.2"
|
||||
@ -45,6 +45,7 @@ runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", ve
|
||||
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
|
||||
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
|
||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
|
||||
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
|
||||
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
@ -83,9 +84,6 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
|
||||
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
|
||||
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
|
||||
|
||||
# Plugin API
|
||||
plugin-api = { group = "app.revanced", name = "revanced-manager-downloader-api", version.ref = "plugin-api" }
|
||||
|
||||
# Koin
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
|
||||
@ -146,3 +144,4 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref =
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
|
||||
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
|
||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
@ -1,16 +1,19 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
mavenLocal()
|
||||
maven {
|
||||
// A repository must be specified for some reason. "registry" is a dummy.
|
||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||
@ -21,6 +24,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "ReVanced Manager"
|
||||
include(":app")
|
||||
include(":downloader-plugin")
|
||||
include(":example-downloader-plugin")
|
||||
|
Reference in New Issue
Block a user