Compare commits

..

5 Commits

Author SHA1 Message Date
oSumAtrIX
65b5b844a8 improve 2026-03-13 00:45:44 +01:00
Ushie
8477369ef1 Merge branch 'dev' into feat/more-logs 2026-03-12 20:50:04 +03:00
Ushie
c322129afa apply suggestions 2026-02-25 01:30:58 +03:00
Ushie
26f6774f7b feat: More info 2026-02-23 02:16:33 +03:00
Ushie
98a0ed55eb feat(PatcherScreen): Export more info in logs 2026-02-23 01:48:40 +03:00
22 changed files with 260 additions and 260 deletions

View File

@@ -1,10 +1,3 @@
# 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,6 +4,7 @@ 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
@@ -14,7 +15,8 @@ group = "app.revanced"
dependencies {
implementation(libs.androidx.ktx)
implementation(libs.runtime.ktx)
implementation(libs.fragment.ktx)
implementation(libs.activity.compose)
implementation(libs.appcompat)
}
kotlin {

View File

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

View File

@@ -6,17 +6,13 @@ 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,
@@ -38,11 +34,6 @@ interface Scope {
* The package name of the downloader.
*/
val downloaderPackageName: String
/**
* A data directory for this downloader package.
*/
val dataDir: File
}
/**
@@ -58,25 +49,6 @@ 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,10 +3,8 @@ 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
/**
@@ -33,15 +31,6 @@ 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,7 +1,6 @@
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
@@ -9,7 +8,6 @@ import app.revanced.manager.downloader.GetScope
import app.revanced.manager.downloader.Scope
import app.revanced.manager.downloader.Downloader
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.requestStartFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -123,11 +121,15 @@ suspend fun <T> GetScope.runWebView(
val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) }
scope.initialUrl = scope.block()
// Start the webview and wait until it finishes.
requestStartFragment<WebViewFragment>(Bundle().apply {
putParcelable(
WebViewFragment.KEY,
WebViewFragment.Parameters(title, scope.binder)
// 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!!
)
})

View File

@@ -1,26 +1,20 @@
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.core.view.MenuProvider
import androidx.activity.viewModels
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
@@ -35,59 +29,34 @@ import kotlinx.parcelize.Parcelize
@OptIn(DownloaderHostApi::class)
@DownloaderHostApi
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
)
}
}
class WebViewActivity : ComponentActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view.findViewById(R.id.main)) { v, insets ->
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
}
webView = view.findViewById<WebView>(R.id.webview)
val webView = findViewById<WebView>(R.id.webview)
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) webView.goBack()
else cancelActivity()
}
val events = IWebViewEvents.Stub.asInterface(args.events)!!
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 {
@@ -104,14 +73,13 @@ class WebViewFragment : Fragment(R.layout.webview_fragment) {
}
}
val activity = requireActivity()
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect {
when (it) {
is WebViewModel.Command.Finish -> {
activity.setResult(Activity.RESULT_OK)
activity.finish()
setResult(RESULT_OK)
finish()
}
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
@@ -122,11 +90,16 @@ class WebViewFragment : Fragment(R.layout.webview_fragment) {
}
private fun cancelActivity() {
val activity = requireActivity()
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
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

View File

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

View 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>

View File

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

View File

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

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

View File

@@ -2,7 +2,6 @@ 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
@@ -25,7 +24,6 @@ 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)
@@ -52,8 +50,6 @@ class DownloaderRepository(
installed subtract acknowledged
}
private val downloadersDir = app.getDir("downloaders", Context.MODE_PRIVATE)
suspend fun reload() {
val downloaderPackages =
withContext(Dispatchers.IO) {
@@ -69,12 +65,8 @@ 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() }
}
}
}
@@ -117,8 +109,6 @@ 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(
@@ -136,12 +126,11 @@ class DownloaderRepository(
className,
downloaderContext.getString(downloader.name),
packageInfo.versionName!!,
scopeImpl,
downloader
downloader.get,
downloader.download
)
},
classLoader,
downloaderContext,
with(pm) { packageInfo.label() }
)
} catch (e: CancellationException) {

View File

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

View File

@@ -38,7 +38,6 @@ 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
@@ -186,7 +185,11 @@ class PatcherWorker(
downloaderRepository.loadedDownloadersFlow.first()
.firstNotNullOfOrNull { downloader ->
try {
val getScope = object : GetScope, Scope by downloader.scopeImpl {
val getScope = object : GetScope {
override val downloaderPackageName = downloader.packageName
override val hostPackageName =
applicationContext.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? {
val result =
args.handleStartActivityRequest(downloader, intent)
@@ -201,7 +204,7 @@ class PatcherWorker(
}
}
withContext(Dispatchers.IO) {
downloader.impl.get(
downloader.get(
getScope,
selectedApp.packageName,
selectedApp.version

View File

@@ -1,10 +1,15 @@
package app.revanced.manager.ui.viewmodel
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.ParcelUuid
import android.os.StatFs
import android.text.format.Formatter
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.compose.runtime.derivedStateOf
@@ -21,11 +26,13 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import androidx.work.WorkInfo
import androidx.work.WorkManager
import app.revanced.manager.BuildConfig
import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.WorkerRepository
@@ -35,6 +42,8 @@ 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.patch.PatchBundleInfo
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.SelectedApp
@@ -92,6 +101,7 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val rootInstaller: RootInstaller by inject()
private val prefs: PreferencesManager by inject()
private val savedStateHandle: SavedStateHandle = get()
private val ackpineInstaller: PackageInstaller = get()
@@ -109,8 +119,7 @@ class PatcherViewModel(
}
private set
var packageInstallerStatus: Int? by savedStateHandle.saveable(
key = "packageInstallerStatus",
stateSaver = autoSaver()
key = "packageInstallerStatus", stateSaver = autoSaver()
) {
mutableStateOf(null)
}
@@ -283,17 +292,14 @@ class PatcherViewModel(
is ProgressEvent.Progress -> withState(
message = event.message ?: message,
progress = event.current?.let { event.current to event.total } ?: progress
)
progress = event.current?.let { event.current to event.total } ?: progress)
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
is ProgressEvent.Failed -> {
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
withState(
State.FAILED,
message = event.error.stackTrace,
progress = null
State.FAILED, message = event.error.stackTrace, progress = null
)
}
}
@@ -330,18 +336,111 @@ class PatcherViewModel(
}
}
fun exportLogs(context: Context) {
fun exportLogs(context: Context) = viewModelScope.launch {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo().also {
activityManager.getMemoryInfo(it)
}
val statFs = StatFs(Environment.getDataDirectory().path)
val hasRoot = rootInstaller.hasRootAccess()
val suggestedVersion = patchBundleRepository.suggestedVersions.first()[packageName]
val allowIncompatiblePatches = prefs.disablePatchVersionCompatCheck.get()
val disableSelectionWarning = prefs.disableSelectionWarning.get()
val disableUniversalPatchCheck = prefs.disableUniversalPatchCheck.get()
val usePatchesPrereleases = prefs.usePatchesPrereleases.get()
val useProcessRuntime = prefs.useProcessRuntime.get()
val patcherProcessMemoryLimit = prefs.patcherProcessMemoryLimit.get()
val apiUrl = prefs.api.get()
val useManagerPrereleases = prefs.useManagerPrereleases.get()
val managerAutoUpdates = prefs.managerAutoUpdates.get()
val patchSelectionChanges = formatPatchSelectionChanges(
patchBundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version).first(),
input.selectedPatches,
allowIncompatiblePatches
)
val patchingConfiguration = buildList {
addPreferenceChange(
"Version compatibility check",
allowIncompatiblePatches,
prefs.disablePatchVersionCompatCheck.default
) { (!it).toString() }
addPreferenceChange(
"Allow changing patch selection",
disableSelectionWarning,
prefs.disableSelectionWarning.default
)
addPreferenceChange(
"Show universal patches",
disableUniversalPatchCheck,
prefs.disableUniversalPatchCheck.default
) { (!it).toString() }
addPreferenceChange(
"Use patches pre-releases",
usePatchesPrereleases,
prefs.usePatchesPrereleases.default
)
}
val runtimeConfiguration = buildList {
addPreferenceChange(
"Use process runtime",
useProcessRuntime,
prefs.useProcessRuntime.default
)
addPreferenceChange(
"Process runtime custom memory limit",
patcherProcessMemoryLimit,
prefs.patcherProcessMemoryLimit.default
) { "${it}MB" }
add("Memory limit: ${activityManager.memoryClass}MB (large: ${activityManager.largeMemoryClass}MB)")
}
val managerConfiguration = buildList {
add("Manager version: ${BuildConfig.VERSION_NAME}")
addPreferenceChange("API URL", apiUrl, prefs.api.default)
addPreferenceChange(
"Use manager pre-releases",
useManagerPrereleases,
prefs.useManagerPrereleases.default
)
addPreferenceChange(
"Manager auto-update",
managerAutoUpdates,
prefs.managerAutoUpdates.default
)
}
val details = buildList {
add(formatAppLine(packageName, selectedApp.version, suggestedVersion))
addAll(patchSelectionChanges)
addAll(managerConfiguration)
addAll(patchingConfiguration)
addAll(runtimeConfiguration)
add("Root permissions: ${if (hasRoot) "Yes" else "No"}")
add("RAM: ${Formatter.formatFileSize(context, memInfo.availMem)} / ${Formatter.formatFileSize(context, memInfo.totalMem)} available")
add("Storage: ${Formatter.formatFileSize(context, statFs.availableBytes)} / ${Formatter.formatFileSize(context, statFs.totalBytes)} available")
add("Android version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
add("Supported architectures: ${Build.SUPPORTED_ABIS.joinToString()}")
add("Model: ${Build.MODEL}")
}.joinToString("\n")
val logsContent =
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
Intent.EXTRA_TEXT, "$details\n\n$logsContent"
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
context.startActivity(Intent.createChooser(sendIntent, null))
}
fun open() = installedPackageName?.let(pm::launch)
@@ -385,8 +484,9 @@ class PatcherViewModel(
installedAppRepository.addOrUpdate(
installerPkgName,
packageName,
input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
input.selectedApp.version ?: withContext(Dispatchers.IO) {
pm.getPackageInfo(outputFile)?.versionName!!
},
InstallType.DEFAULT,
input.selectedPatches,
bundleInfo
@@ -446,11 +546,7 @@ class PatcherViewModel(
needsRootUninstall = true
// Install as root
rootInstaller.install(
outputFile,
inputFile,
packageName,
inputVersion,
label
outputFile, inputFile, packageName, inputVersion, label
)
val bundleInfo = patchBundleRepository.bundleInfoFlow.first()
@@ -531,19 +627,66 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg)
}
fun formatAppLine(packageName: String, selectedVersion: String?, suggestedVersion: String?): String {
val versionDetails = buildList {
selectedVersion?.let { add("Selected: $it") }
suggestedVersion?.let { add("Suggested: $it") }
}
return if (versionDetails.isEmpty()) {
"App: $packageName"
} else {
"App: $packageName (${versionDetails.joinToString(", ")})"
}
}
inline fun <T> MutableList<String>.addPreferenceChange(
label: String,
value: T,
default: T,
formatter: (T) -> String = { it.toString() }
) {
if (value != default) add("$label: ${formatter(value)}")
}
fun formatPatchSelectionChanges(
bundles: List<PatchBundleInfo.Scoped>,
selectedPatches: PatchSelection,
allowIncompatible: Boolean
): List<String> {
val defaultSelection = bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
val bundleNames = bundles.associate { it.uid to it.name }
val knownBundleIds = bundles.map(PatchBundleInfo.Scoped::uid)
val orderedBundleIds = knownBundleIds + (selectedPatches.keys + defaultSelection.keys)
.filterNot(knownBundleIds::contains)
.sorted()
return buildList {
orderedBundleIds.distinct().forEach { uid ->
val selected = selectedPatches[uid].orEmpty()
val defaults = defaultSelection[uid].orEmpty()
val added = (selected - defaults).sorted()
val removed = (defaults - selected).sorted()
if (added.isEmpty() && removed.isEmpty()) return@forEach
add("Source: ${bundleNames[uid] ?: "Source $uid"}")
if (added.isNotEmpty()) add("Added: ${added.joinToString()}")
if (removed.isNotEmpty()) add("Removed: ${removed.joinToString()}")
}
}
}
fun generateSteps(
context: Context,
selectedApp: SelectedApp,
selectedPatches: PatchSelection
context: Context, selectedApp: SelectedApp, selectedPatches: PatchSelection
): List<Step> = buildList {
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
add(
Step(
StepId.DownloadAPK,
context.getString(R.string.download_apk),
StepCategory.PREPARING
)
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search) add(
Step(
StepId.DownloadAPK,
context.getString(R.string.download_apk),
StepCategory.PREPARING
)
)
add(
Step(
@@ -571,9 +714,7 @@ class PatcherViewModel(
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
add(
Step(
StepId.ExecutePatch(index),
name,
StepCategory.PATCHING
StepId.ExecutePatch(index), name, StepCategory.PATCHING
)
)
}
@@ -594,4 +735,7 @@ class PatcherViewModel(
)
}
}
}
}

View File

@@ -34,7 +34,6 @@ 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
@@ -193,7 +192,9 @@ class SelectedAppInfoViewModel(
cancelDownloaderAction()
downloaderAction = downloader to viewModelScope.launch {
try {
val scope = object : GetScope, Scope by downloader.scopeImpl {
val scope = object : GetScope {
override val hostPackageName = app.packageName
override val downloaderPackageName = downloader.packageName
override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished")
@@ -218,7 +219,7 @@ class SelectedAppInfoViewModel(
}
withContext(Dispatchers.IO) {
downloader.impl.get(scope, packageName, desiredVersion)
downloader.get(scope, packageName, desiredVersion)
}?.let { (data, version) ->
if (desiredVersion != null && version != desiredVersion) {
app.toast(app.getString(R.string.downloader_invalid_version))

View File

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

View File

@@ -6,9 +6,4 @@
<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

@@ -1,12 +1,11 @@
[versions]
ktx = "1.18.0"
ktx = "1.17.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"
@@ -14,15 +13,15 @@ navigation = "2.9.7"
accompanist = "0.37.3"
placeholder = "1.0.12"
reorderable = "3.0.0"
serialization = "1.10.0"
serialization = "1.9.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.4.1"
markdown-renderer = "0.39.2"
ktor = "3.3.3"
markdown-renderer = "0.39.0"
fading-edges = "1.0.4"
kotlin = "2.3.10"
android-gradle-plugin = "8.13.2"
@@ -38,7 +37,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.20.6"
ackpine = "0.19.1"
[libraries]
# AndroidX Core
@@ -47,7 +46,6 @@ 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" }