Compare commits

..

7 Commits

Author SHA1 Message Date
semantic-release-bot
c53617d92d chore: Release API v1.0.0-dev.3 [skip ci]
# api [1.0.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.2...api@1.0.0-dev.3) (2026-03-12)

### Features

* Add support for writing downloader UI with fragments ([#2998](https://github.com/ReVanced/revanced-manager/issues/2998)) ([7471710](7471710f28))
2026-03-12 20:48:48 +00:00
Ax333l
7471710f28 feat: Add support for writing downloader UI with fragments (#2998) 2026-03-12 21:41:34 +01:00
Ushie
65babf5190 chore: Merge plurals.xml into strings.xml 2026-03-12 13:59:46 +03:00
Pun Butrach
d937e686f8 ci: Bump CI dependencies before Node 20 deprecation 2026-03-10 20:26:22 +07:00
semantic-release-bot
b0016f7c05 chore: Release v1.26.0-dev.34 [skip ci]
# app [1.26.0-dev.34](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.33...v1.26.0-dev.34) (2026-03-10)

### Features

* Improve root installation ([#2895](https://github.com/ReVanced/revanced-manager/issues/2895)) ([763483b](763483b65f))
2026-03-10 01:08:55 +00:00
Robert
763483b65f feat: Improve root installation (#2895)
Co-authored-by: Ushie <ushiekane@gmail.com>
2026-03-10 04:01:49 +03:00
Ushie
c59b3e4377 build: Enable build parallelism 2026-03-10 04:00:59 +03:00
34 changed files with 335 additions and 154 deletions

View File

@@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
@@ -24,7 +24,7 @@ jobs:
run: ./gradlew assembleRelease --no-daemon
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: revanced-manager
path: |

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Open pull request
uses: repo-sync/pull-request@v2

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: dev
clean: true

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Push strings
uses: crowdin/github-action@v2

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
@@ -37,7 +37,7 @@ jobs:
run: ./gradlew assembleRelease
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "lts/*"
cache: 'npm'
@@ -46,7 +46,7 @@ jobs:
run: npm ci
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}

View File

@@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v3
- uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation

View File

@@ -1,3 +1,10 @@
# api [1.0.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.2...api@1.0.0-dev.3) (2026-03-12)
### Features
* Add support for writing downloader UI with fragments ([#2998](https://github.com/ReVanced/revanced-manager/issues/2998)) ([7471710](https://github.com/ReVanced/revanced-manager/commit/7471710f287b4febb9546a7d7cd59d654b2754e9))
# api [1.0.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.1...api@1.0.0-dev.2) (2026-02-27)

View File

@@ -4,7 +4,6 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
signing
@@ -15,8 +14,7 @@ group = "app.revanced"
dependencies {
implementation(libs.androidx.ktx)
implementation(libs.runtime.ktx)
implementation(libs.activity.compose)
implementation(libs.appcompat)
implementation(libs.fragment.ktx)
}
kotlin {

View File

@@ -1 +1 @@
version = 1.0.0-dev.2
version = 1.0.0-dev.3

View File

@@ -6,13 +6,17 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.app.Activity
import android.os.Bundle
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import kotlinx.coroutines.withTimeout
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.jvm.java
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
@@ -34,6 +38,11 @@ interface Scope {
* The package name of the downloader.
*/
val downloaderPackageName: String
/**
* A data directory for this downloader package.
*/
val dataDir: File
}
/**
@@ -49,6 +58,25 @@ interface GetScope : Scope {
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
suspend fun requestStartActivity(intent: Intent): Intent?
/**
* Starts an [Activity] using [requestStartActivity] which loads the specified [Fragment].
* The fragment may reside in the downloader package.
*
* @param clazz The class of the fragment to launch.
* @param args The fragment arguments.
*/
suspend fun requestStartFragment(clazz: Class<out Fragment>, args: Bundle?) =
requestStartActivity(Intent().apply {
setClassName(hostPackageName, "app.revanced.manager.DownloaderActivity")
// We shouldn't use the downloader's resources if it is launching a fragment that resides in manager itself.
if (clazz.classLoader != GetScope::class.java.classLoader) putExtra(
"DOWNLOADER_NAME",
downloaderPackageName
)
putExtra("FRAGMENT_CLASS_NAME", clazz.name)
putExtra("FRAGMENT_ARGS", args)
})
}
interface BaseDownloadScope : Scope

View File

@@ -3,8 +3,10 @@ package app.revanced.manager.downloader
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import androidx.fragment.app.Fragment
import java.io.OutputStream
/**
@@ -31,6 +33,15 @@ suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity()
Intent().apply { setClassName(downloaderPackageName, ACTIVITY::class.qualifiedName!!) }
)
/**
* Starts an [Activity] using [GetScope.requestStartActivity] which loads the specified [Fragment].
* The fragment may reside in the downloader package.
*
* @param args The fragment arguments.
*/
suspend inline fun <reified T : Fragment> GetScope.requestStartFragment(args: Bundle?) =
requestStartFragment(T::class.java, args)
/**
* Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE].
* @see [DownloaderScope.useService]

View File

@@ -1,6 +1,7 @@
package app.revanced.manager.downloader.webview
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StringRes
import app.revanced.manager.downloader.DownloadUrl
import app.revanced.manager.downloader.DownloaderScope
@@ -8,6 +9,7 @@ import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.Scope
import app.revanced.manager.downloader.Downloader
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.requestStartFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -121,15 +123,11 @@ suspend fun <T> GetScope.runWebView(
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!!
// Start the webview and wait until it finishes.
requestStartFragment<WebViewFragment>(Bundle().apply {
putParcelable(
WebViewFragment.KEY,
WebViewFragment.Parameters(title, scope.binder)
)
})

View File

@@ -1,20 +1,26 @@
package app.revanced.manager.downloader.webview
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.IBinder
import android.os.Parcelable
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
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.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
@@ -29,34 +35,59 @@ import kotlinx.parcelize.Parcelize
@OptIn(DownloaderHostApi::class)
@DownloaderHostApi
class WebViewActivity : ComponentActivity() {
class WebViewFragment : Fragment(R.layout.webview_fragment) {
private val vm by viewModels<WebViewModel>()
lateinit var webView: WebView
private val args by lazy {
arguments?.getParcelable<Parameters>(KEY)!!
}
override fun onAttach(context: Context) {
super.onAttach(context)
requireActivity().apply {
enableEdgeToEdge()
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) webView.goBack()
else cancelActivity()
}
actionBar?.apply {
title = args.title
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
setDisplayHomeAsUpEnabled(true)
}
addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(
menu: Menu,
menuInflater: MenuInflater
) {
}
override fun onMenuItemSelected(menuItem: MenuItem) =
if (menuItem.itemId == android.R.id.home) {
cancelActivity()
true
} else false
},
this
)
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val vm by viewModels<WebViewModel>()
enableEdgeToEdge()
setContentView(R.layout.activity_webview)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
ViewCompat.setOnApplyWindowInsetsListener(view.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()
}
webView = view.findViewById<WebView>(R.id.webview)
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)!!
val events = IWebViewEvents.Stub.asInterface(args.events)!!
vm.setup(events)
webView.apply {
@@ -73,13 +104,14 @@ class WebViewActivity : ComponentActivity() {
}
}
val activity = requireActivity()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect {
when (it) {
is WebViewModel.Command.Finish -> {
setResult(RESULT_OK)
finish()
activity.setResult(Activity.RESULT_OK)
activity.finish()
}
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
@@ -90,16 +122,11 @@ class WebViewActivity : ComponentActivity() {
}
private fun cancelActivity() {
setResult(RESULT_CANCELED)
finish()
val activity = requireActivity()
activity.setResult(Activity.RESULT_CANCELED)
activity.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

View File

@@ -1 +0,0 @@
<resources></resources>

View File

@@ -1,7 +0,0 @@
<?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>

View File

@@ -1,3 +1,10 @@
# app [1.26.0-dev.34](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.33...v1.26.0-dev.34) (2026-03-10)
### Features
* Improve root installation ([#2895](https://github.com/ReVanced/revanced-manager/issues/2895)) ([763483b](https://github.com/ReVanced/revanced-manager/commit/763483b65f755201db087eecc6f3b02b65921177))
# app [1.26.0-dev.33](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.32...v1.26.0-dev.33) (2026-03-10)

View File

@@ -1 +1 @@
version = 1.26.0-dev.33
version = 1.26.0-dev.34

View File

@@ -49,7 +49,7 @@
</intent-filter>
</activity>
<activity android:name=".downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<activity android:name=".DownloaderActivity" android:exported="false" android:theme="@style/Theme.DownloaderActivity" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -0,0 +1,62 @@
package app.revanced.manager
import android.content.res.Resources
import android.os.Bundle
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.commit
import app.revanced.manager.domain.repository.DownloaderRepository
import app.revanced.manager.network.downloader.DownloaderPackageState
import org.koin.android.ext.android.inject
class DownloaderActivity : FragmentActivity() {
private val downloaderRepository: DownloaderRepository by inject()
var downloaderPackageName = ""
private val downloaderPkgState
get() = downloaderRepository
.downloaderPackageStates
.value[downloaderPackageName]
?.let { it as? DownloaderPackageState.Loaded }
private var res: Resources? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = FragmentContainerView(this).apply {
// The fragment manager requires an ID to work.
id = R.id.fragment_container
}
setContentView(
view,
ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
)
downloaderPackageName = intent.getStringExtra("DOWNLOADER_NAME").orEmpty()
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")!!
val args = intent.getBundleExtra("FRAGMENT_ARGS")
res =
downloaderPkgState?.context?.createConfigurationContext(super.resources.configuration)?.resources
if (savedInstanceState == null) {
@Suppress("UNCHECKED_CAST")
val fragmentClass = classLoader!!.loadClass(fragmentClassName) as Class<Fragment>
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.fragment_container, fragmentClass, args)
}
}
}
override fun getClassLoader(): ClassLoader? =
downloaderPkgState?.classLoader ?: super.classLoader
override fun getResources(): Resources? = res ?: super.resources
}

View File

@@ -54,7 +54,11 @@ class RootInstaller(
await()
}
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
suspend fun execute(vararg commands: String): Shell.Result {
val stdout = mutableListOf<String>()
val stderr = mutableListOf<String>()
return getShell().newJob().add(*commands).to(stdout, stderr).exec()
}
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
@@ -108,20 +112,18 @@ class RootInstaller(
unmount(packageName)
stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo ->
// TODO: get user id programmatically
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
pm.getPackageInfo(patchedAPK)
?: error("Failed to get package info for patched app")
)
)
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
}
// TODO: get user id programmatically
execute("pm uninstall -k --user 0 $packageName")
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"")
.assertSuccess("Failed to install stock app")
}
remoteFS.getFile(modulePath).mkdirs()
remoteFS.getFile(modulePath).apply {
if (!mkdirs() && !exists()) {
throw Exception("Failed to create module directory")
}
}
listOf(
"service.sh",
@@ -142,7 +144,6 @@ class RootInstaller(
}
"$modulePath/$packageName.apk".let { apkPath ->
remoteFS.getFile(patchedAPK.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream ->
@@ -173,9 +174,48 @@ class RootInstaller(
const val modulesPath = "/data/adb/modules"
private fun Shell.Result.assertSuccess(errorMessage: String) {
if (!isSuccess) throw Exception(errorMessage)
if (!isSuccess) {
throw ShellCommandException(
errorMessage,
code,
out,
err
)
}
}
}
}
class ShellCommandException(
val userMessage: String,
val exitCode: Int,
val stdout: List<String>,
val stderr: List<String>
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
companion object {
private fun format(
message: String,
exitCode: Int,
stdout: List<String>,
stderr: List<String>
): String =
buildString {
appendLine(message)
appendLine("Exit code: $exitCode")
val output = stdout.filter { it.isNotBlank() }
val errors = stderr.filter { it.isNotBlank() }
if (output.isNotEmpty()) {
appendLine("stdout:")
output.forEach(::appendLine)
}
if (errors.isNotEmpty()) {
appendLine("stderr:")
errors.forEach(::appendLine)
}
}
}
}
class RootServiceException : Exception("Root not available")

View File

@@ -6,8 +6,10 @@ import android.os.Parcelable
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.network.downloader.LoadedDownloader
import app.revanced.manager.downloader.OutputDownloadScope
import app.revanced.manager.downloader.Scope
import app.revanced.manager.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
@@ -21,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
import kotlin.io.path.outputStream
class DownloadedAppRepository(
private val app: Application,
app: Application,
db: AppDatabase,
private val pm: PM
) {
@@ -35,6 +37,7 @@ class DownloadedAppRepository(
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
@OptIn(DownloaderHostApi::class)
suspend fun download(
downloader: LoadedDownloader,
data: Parcelable,
@@ -54,9 +57,7 @@ class DownloadedAppRepository(
val downloadedBytes = AtomicLong(0)
channelFlow {
val scope = object : OutputDownloadScope {
override val downloaderPackageName = downloader.packageName
override val hostPackageName = app.packageName
val scope = object : OutputDownloadScope, Scope by downloader.scopeImpl {
override suspend fun reportSize(size: Long) {
require(size > 0) { "Size must be greater than zero" }
require(
@@ -87,7 +88,7 @@ class DownloadedAppRepository(
)
}
}
downloader.download(scope, data, stream)
downloader.impl.download(scope, data, stream)
}
}
.conflate()

View File

@@ -2,6 +2,7 @@ package app.revanced.manager.domain.repository
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.pm.PackageInfo
import android.os.Parcelable
import android.util.Log
@@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.io.File
import java.lang.reflect.Modifier
@OptIn(DownloaderHostApi::class)
@@ -50,6 +52,8 @@ class DownloaderRepository(
installed subtract acknowledged
}
private val downloadersDir = app.getDir("downloaders", Context.MODE_PRIVATE)
suspend fun reload() {
val downloaderPackages =
withContext(Dispatchers.IO) {
@@ -65,8 +69,12 @@ class DownloaderRepository(
acknowledgedDownloader subtract installedDownloaderPackageNames.value
if (uninstalledDownloader.isNotEmpty()) {
Log.d(tag, "Uninstalled downloader: ${uninstalledDownloader.joinToString(", ")}")
this@DownloaderRepository.acknowledgedPackageNames.update(acknowledgedDownloader subtract uninstalledDownloader)
trustDao.removeAll(uninstalledDownloader)
withContext(Dispatchers.IO) {
uninstalledDownloader.forEach { downloadersDir.resolve(it).deleteRecursively() }
}
}
}
@@ -109,6 +117,8 @@ class DownloaderRepository(
val scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloaderContext.packageName
override val dataDir =
downloadersDir.resolve(downloaderPackageName).also(File::mkdirs)
}
DownloaderPackageState.Loaded(
@@ -126,11 +136,12 @@ class DownloaderRepository(
className,
downloaderContext.getString(downloader.name),
packageInfo.versionName!!,
downloader.get,
downloader.download
scopeImpl,
downloader
)
},
classLoader,
downloaderContext,
with(pm) { packageInfo.label() }
)
} catch (e: CancellationException) {

View File

@@ -1,11 +1,14 @@
package app.revanced.manager.network.downloader
import android.content.Context
sealed interface DownloaderPackageState {
data object Untrusted : DownloaderPackageState
data class Loaded(
val downloaders: List<LoadedDownloader>,
val classLoader: ClassLoader,
val context: Context,
val name: String
) : DownloaderPackageState

View File

@@ -1,15 +1,14 @@
package app.revanced.manager.network.downloader
import android.os.Parcelable
import app.revanced.manager.downloader.OutputDownloadScope
import app.revanced.manager.downloader.GetScope
import java.io.OutputStream
import app.revanced.manager.downloader.Downloader
import app.revanced.manager.downloader.Scope
class LoadedDownloader(
val packageName: String,
val className: String,
val name: String,
val version: String,
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit
val scopeImpl: Scope,
val impl: Downloader<Parcelable>
)

View File

@@ -38,6 +38,7 @@ import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.patcher.toRemoteError
import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.Scope
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
@@ -185,11 +186,7 @@ class PatcherWorker(
downloaderRepository.loadedDownloadersFlow.first()
.firstNotNullOfOrNull { downloader ->
try {
val getScope = object : GetScope {
override val downloaderPackageName = downloader.packageName
override val hostPackageName =
applicationContext.packageName
val getScope = object : GetScope, Scope by downloader.scopeImpl {
override suspend fun requestStartActivity(intent: Intent): Intent? {
val result =
args.handleStartActivityRequest(downloader, intent)
@@ -204,7 +201,7 @@ class PatcherWorker(
}
}
withContext(Dispatchers.IO) {
downloader.get(
downloader.impl.get(
getScope,
selectedApp.packageName,
selectedApp.version

View File

@@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller as AndroidPackageInstaller
import android.net.Uri
import android.os.ParcelUuid
import android.util.Log
@@ -30,18 +29,18 @@ import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.withState
import app.revanced.manager.util.PM
@@ -80,6 +79,7 @@ import ru.solrudev.ackpine.uninstaller.UninstallFailure
import java.io.File
import java.nio.file.Files
import java.time.Duration
import android.content.pm.PackageInstaller as AndroidPackageInstaller
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class PatcherViewModel(
@@ -404,24 +404,26 @@ class PatcherViewModel(
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
?: throw Exception("Failed to load application info")
// If the app is currently installed
val existingPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
existingPackageInfo
)
) {
// Exit if the selected app version is less than the installed version
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
when (installType) {
InstallType.DEFAULT -> {
// If the app is currently installed
val existingPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
existingPackageInfo
)
) {
// Exit if the selected app version is less than the installed version
packageInstallerStatus =
AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
// Check if the app is mounted as root
// If it is, unmount it first, silently
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
@@ -437,17 +439,6 @@ class PatcherViewModel(
currentPackageInfo.label()
}
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package
packageInstallerStatus =
AndroidPackageInstaller.STATUS_FAILURE_INVALID
return@launch
}
}
val inputVersion = input.selectedApp.version
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
?: throw Exception("Failed to determine input APK version")

View File

@@ -34,6 +34,7 @@ import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.Scope
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher
@@ -192,9 +193,7 @@ class SelectedAppInfoViewModel(
cancelDownloaderAction()
downloaderAction = downloader to viewModelScope.launch {
try {
val scope = object : GetScope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloader.packageName
val scope = object : GetScope, Scope by downloader.scopeImpl {
override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished")
@@ -219,7 +218,7 @@ class SelectedAppInfoViewModel(
}
withContext(Dispatchers.IO) {
downloader.get(scope, packageName, desiredVersion)
downloader.impl.get(scope, packageName, desiredVersion)
}?.let { (data, version) ->
if (desiredVersion != null && version != desiredVersion) {
app.toast(app.getString(R.string.downloader_invalid_version))

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="fragment_container" />
</resources>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="patch_count">
<item quantity="one">%d patch</item>
<item quantity="other">%d patches</item>
</plurals>
<plurals name="patches_executed">
<item quantity="one">Execute %d patch</item>
<item quantity="other">Execute %d patches</item>
</plurals>
<plurals name="selected_count">
<item quantity="other">%d selected</item>
</plurals>
</resources>

View File

@@ -516,4 +516,16 @@ Click on the patches to see more details.</string>
<string name="export">Export</string>
<string name="confirm">Confirm</string>
<string name="new_announcement">New announcement:\n%s</string>
<plurals name="patch_count">
<item quantity="one">%d patch</item>
<item quantity="other">%d patches</item>
</plurals>
<plurals name="patches_executed">
<item quantity="one">Execute %d patch</item>
<item quantity="other">Execute %d patches</item>
</plurals>
<plurals name="selected_count">
<item quantity="other">%d selected</item>
</plurals>
</resources>

View File

@@ -6,4 +6,9 @@
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="Theme.DownloaderActivity" parent="Theme.AppCompat.DayNight">
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
</style>
</resources>

View File

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

View File

@@ -1,11 +1,12 @@
[versions]
ktx = "1.17.0"
ktx = "1.18.0"
material3 = "1.5.0-alpha14"
ui-tooling = "1.10.0"
viewmodel-lifecycle = "2.10.0"
splash-screen = "1.2.0"
activity = "1.12.2"
appcompat = "1.7.1"
fragment = "1.8.9"
preferences-datastore = "1.2.0"
work-runtime = "2.11.0"
compose-bom = "2025.12.01"
@@ -13,15 +14,15 @@ navigation = "2.9.7"
accompanist = "0.37.3"
placeholder = "1.0.12"
reorderable = "3.0.0"
serialization = "1.9.0"
serialization = "1.10.0"
collection = "0.4.0"
datetime = "0.7.1"
room-version = "2.8.4"
revanced-patcher = "22.0.0"
revanced-library = "4.0.0"
koin = "4.1.1"
ktor = "3.3.3"
markdown-renderer = "0.39.0"
ktor = "3.4.1"
markdown-renderer = "0.39.2"
fading-edges = "1.0.4"
kotlin = "2.3.10"
android-gradle-plugin = "8.13.2"
@@ -37,7 +38,7 @@ kotlin-process = "1.5.1"
hidden-api-stub = "4.4.0"
binary-compatibility-validator = "0.18.1"
semver-parser = "3.0.0"
ackpine = "0.19.1"
ackpine = "0.20.6"
[libraries]
# AndroidX Core
@@ -46,6 +47,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" }
fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" }
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" }