Compare commits

..

1 Commits

Author SHA1 Message Date
Robert
ff70a77afb feat: Improve root installation 2025-12-26 01:07:58 +01:00
18 changed files with 444 additions and 409 deletions

View File

@@ -1,17 +1,3 @@
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
### Bug Fixes
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
### Bug Fixes
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17) # app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)

View File

@@ -108,10 +108,6 @@ dependencies {
// Compose Icons // Compose Icons
implementation(libs.compose.icons.fontawesome) implementation(libs.compose.icons.fontawesome)
// Ackpine
implementation(libs.ackpine.core)
implementation(libs.ackpine.ktx)
} }
buildscript { buildscript {

View File

@@ -1 +1 @@
version = 1.26.0-dev.15 version = 1.26.0-dev.13

View File

@@ -51,6 +51,9 @@
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" /> <activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
<service android:name=".service.InstallService" />
<service android:name=".service.UninstallService" />
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
@@ -72,15 +75,5 @@
android:value="androidx.startup" android:value="androidx.startup"
tools:node="remove" /> tools:node="remove" />
</provider> </provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="ru.solrudev.ackpine.AckpineInitializer"
tools:node="remove" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -48,8 +48,7 @@ class ManagerApplication : Application() {
workerModule, workerModule,
viewModelModule, viewModelModule,
databaseModule, databaseModule,
rootModule, rootModule
ackpineModule
) )
} }

View File

@@ -1,19 +0,0 @@
package app.revanced.manager.di
import android.content.Context
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import ru.solrudev.ackpine.installer.PackageInstaller
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
val ackpineModule = module {
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
single {
provideInstaller(androidContext())
}
single {
provideUninstaller(androidContext())
}
}

View File

@@ -54,7 +54,11 @@ class RootInstaller(
await() 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 fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
@@ -108,20 +112,15 @@ class RootInstaller(
unmount(packageName) unmount(packageName)
stockAPK?.let { stockApp -> stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo -> // TODO: get user id programmatically
// TODO: get user id programmatically execute("pm uninstall -k --user 0 $packageName")
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")
}
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).mkdir() remoteFS.getFile(modulePath).mkdirs()
.also { if (!it) throw Exception("Failed to create module directory") }
listOf( listOf(
"service.sh", "service.sh",
@@ -142,7 +141,6 @@ class RootInstaller(
} }
"$modulePath/$packageName.apk".let { apkPath -> "$modulePath/$packageName.apk".let { apkPath ->
remoteFS.getFile(patchedAPK.absolutePath) remoteFS.getFile(patchedAPK.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") } .also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream -> .newInputStream().use { inputStream ->
@@ -173,9 +171,43 @@ class RootInstaller(
const val modulesPath = "/data/adb/modules" const val modulesPath = "/data/adb/modules"
private fun Shell.Result.assertSuccess(errorMessage: String) { 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") class RootServiceException : Exception("Root not available")

View File

@@ -0,0 +1,53 @@
package app.revanced.manager.service
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
@Suppress("DEPRECATION")
class InstallService : Service() {
override fun onStartCommand(
intent: Intent, flags: Int, startId: Int
): Int {
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}.apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}
else -> {
sendBroadcast(Intent().apply {
action = APP_INSTALL_ACTION
`package` = packageName
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
})
}
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
}
}

View File

@@ -1,6 +1,5 @@
package app.revanced.manager.ui.component package app.revanced.manager.ui.component
import android.annotation.SuppressLint
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -80,7 +79,7 @@ private fun installerStatusDialogButton(
enum class DialogKind( enum class DialogKind(
val flag: Int, val flag: Int,
val title: Int, val title: Int,
@param:StringRes val contentStringResId: Int, @StringRes val contentStringResId: Int,
val icon: ImageVector = Icons.Outlined.ErrorOutline, val icon: ImageVector = Icons.Outlined.ErrorOutline,
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
val dismissButton: InstallerStatusDialogButton? = null, val dismissButton: InstallerStatusDialogButton? = null,
@@ -134,8 +133,10 @@ enum class DialogKind(
title = R.string.installation_storage_issue_dialog_title, title = R.string.installation_storage_issue_dialog_title,
contentStringResId = R.string.installation_storage_issue_description, contentStringResId = R.string.installation_storage_issue_description,
), ),
@RequiresApi(34)
FAILURE_TIMEOUT( FAILURE_TIMEOUT(
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT, flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
title = R.string.installation_timeout_dialog_title, title = R.string.installation_timeout_dialog_title,
contentStringResId = R.string.installation_timeout_description, contentStringResId = R.string.installation_timeout_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model -> confirmButton = installerStatusDialogButton(R.string.install_app) { model ->

View File

@@ -25,7 +25,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -77,12 +76,12 @@ fun SelectedAppInfoScreen(
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList()) val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
val patches by remember { val patches = remember(bundles, allowIncompatiblePatches) {
derivedStateOf { vm.getPatches(bundles, allowIncompatiblePatches)
vm.getPatches(bundles, allowIncompatiblePatches) }
} val selectedPatchCount = remember(patches) {
patches.values.sumOf { it.size }
} }
val selectedPatchCount = patches.values.sumOf { it.size }
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),

View File

@@ -1,11 +1,17 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@@ -13,6 +19,7 @@ import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
@@ -23,8 +30,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import ru.solrudev.ackpine.session.Session
import ru.solrudev.ackpine.uninstaller.UninstallFailure
class InstalledAppInfoViewModel( class InstalledAppInfoViewModel(
packageName: String packageName: String
@@ -82,28 +87,51 @@ class InstalledAppInfoViewModel(
fun uninstall() { fun uninstall() {
val app = installedApp ?: return val app = installedApp ?: return
viewModelScope.launch { when (app.installType) {
when (app.installType) { InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
InstallType.DEFAULT -> {
when (val result = pm.uninstallPackage(app.currentPackageName)) {
is Session.State.Failed<UninstallFailure> -> {
val msg = result.failure.message.orEmpty()
context.toast(
this@InstalledAppInfoViewModel.context.getString(
R.string.uninstall_app_fail,
msg
)
)
return@launch
}
Session.State.Succeeded -> {}
}
}
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName) InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(app.currentPackageName)
installedAppRepository.delete(app)
onBackClick()
} }
installedAppRepository.delete(app)
onBackClick()
} }
} }
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UninstallService.APP_UNINSTALL_ACTION -> {
val extraStatus =
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
val extraStatusMessage =
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedApp?.let {
installedAppRepository.delete(it)
}
onBackClick()
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}.also {
ContextCompat.registerReceiver(
context,
it,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
override fun onCleared() {
super.onCleared()
context.unregisterReceiver(uninstallBroadcastReceiver)
}
} }

View File

@@ -1,9 +1,11 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller as AndroidPackageInstaller import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
@@ -14,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
@@ -34,6 +37,8 @@ import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.model.InstallerModel import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
@@ -43,19 +48,16 @@ import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider import app.revanced.manager.ui.model.StepProgressProvider
import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.asCode
import app.revanced.manager.util.saveableVar import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateListSaver
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -64,15 +66,6 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.component.inject import org.koin.core.component.inject
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.installer.PackageInstaller
import ru.solrudev.ackpine.installer.createSession
import ru.solrudev.ackpine.installer.getSession
import ru.solrudev.ackpine.session.ProgressSession
import ru.solrudev.ackpine.session.Session
import ru.solrudev.ackpine.session.await
import ru.solrudev.ackpine.session.parameters.Confirmation
import ru.solrudev.ackpine.uninstaller.UninstallFailure
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.time.Duration import java.time.Duration
@@ -88,7 +81,6 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val ackpineInstaller: PackageInstaller = get()
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
private val selectedApp = input.selectedApp private val selectedApp = input.selectedApp
@@ -103,6 +95,7 @@ class PatcherViewModel(
mutableStateOf<String?>(null) mutableStateOf<String?>(null)
} }
private set private set
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
var packageInstallerStatus: Int? by savedStateHandle.saveable( var packageInstallerStatus: Int? by savedStateHandle.saveable(
key = "packageInstallerStatus", key = "packageInstallerStatus",
stateSaver = autoSaver() stateSaver = autoSaver()
@@ -111,7 +104,7 @@ class PatcherViewModel(
} }
private set private set
var isInstalling by mutableStateOf(false) var isInstalling by mutableStateOf(ongoingPmSession)
private set private set
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf( private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
@@ -130,18 +123,6 @@ class PatcherViewModel(
} }
} }
/**
* This coroutine scope is used to await installations.
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
*/
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
/**
* Holds the package name of the Apk we are trying to install.
*/
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
private var inputFile: File? by savedStateHandle.saveableVar() private var inputFile: File? by savedStateHandle.saveableVar()
private val outputFile = tempDir.resolve("output.apk") private val outputFile = tempDir.resolve("output.apk")
@@ -193,68 +174,67 @@ class PatcherViewModel(
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> { private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
ParcelUuid( ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( "patching", PatcherWorker.Args(
"patching", PatcherWorker.Args( input.selectedApp,
input.selectedApp, outputFile.path,
outputFile.path, input.selectedPatches,
input.selectedPatches, input.options,
input.options, logger,
logger, onDownloadProgress = {
onDownloadProgress = { withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { downloadProgress = it
downloadProgress = it }
} },
}, onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, handleStartActivityRequest = { plugin, intent ->
handleStartActivityRequest = { plugin, intent -> withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { if (currentActivityRequest != null) throw Exception("Another request is already pending.")
if (currentActivityRequest != null) throw Exception("Another request is already pending.") try {
try { // Wait for the dialog interaction.
// Wait for the dialog interaction. val accepted = with(CompletableDeferred<Boolean>()) {
val accepted = with(CompletableDeferred<Boolean>()) { currentActivityRequest = this to plugin.name
currentActivityRequest = this to plugin.name
await()
}
if (!accepted) throw UserInteractionException.RequestDenied()
// Launch the activity and wait for the result.
try {
with(CompletableDeferred<ActivityResult>()) {
launchedActivity = this
launchActivityChannel.send(intent)
await() await()
} }
if (!accepted) throw UserInteractionException.RequestDenied()
// Launch the activity and wait for the result.
try {
with(CompletableDeferred<ActivityResult>()) {
launchedActivity = this
launchActivityChannel.send(intent)
await()
}
} finally {
launchedActivity = null
}
} finally { } finally {
currentActivityRequest = null launchedActivity = null
}
}
},
onProgress = { name, state, message ->
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
copy(
name = name ?: this.name,
state = state ?: this.state,
message = message ?: this.message
)
}
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
currentStepIndex++
steps[currentStepIndex] =
steps[currentStepIndex].copy(state = State.RUNNING)
} }
} finally {
currentActivityRequest = null
} }
} }
) },
)) onProgress = { name, state, message ->
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
copy(
name = name ?: this.name,
state = state ?: this.state,
message = message ?: this.message
)
}
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
currentStepIndex++
steps[currentStepIndex] =
steps[currentStepIndex].copy(state = State.RUNNING)
}
}
}
)
))
} }
val patcherSucceeded = val patcherSucceeded =
@@ -266,26 +246,64 @@ class PatcherViewModel(
} }
} }
init { private val installerBroadcastReceiver = object : BroadcastReceiver() {
// TODO: detect system-initiated process death during the patching process. override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
InstallService.APP_INSTALL_ACTION -> {
val pmStatus = intent.getIntExtra(
InstallService.EXTRA_INSTALL_STATUS,
PackageInstaller.STATUS_FAILURE
)
installerSessionId?.uuid?.let { id -> intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
viewModelScope.launch { ?.let(logger::trace)
try {
isInstalling = true if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
uiSafe(app, R.string.install_app_fail, "Failed to install") { app.toast(app.getString(R.string.install_app_success))
// The process was killed during installation. Await the session again. installedPackageName =
withContext(Dispatchers.IO) { intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
ackpineInstaller.getSession(id) viewModelScope.launch {
}?.let { installedAppRepository.addOrUpdate(
awaitInstallation(it) installedPackageName!!,
packageName,
input.selectedApp.version
?: pm.getPackageInfo(outputFile)?.versionName!!,
InstallType.DEFAULT,
input.selectedPatches
)
} }
} } else packageInstallerStatus = pmStatus
} finally {
isInstalling = false isInstalling = false
} }
UninstallService.APP_UNINSTALL_ACTION -> {
val pmStatus = intent.getIntExtra(
UninstallService.EXTRA_UNINSTALL_STATUS,
PackageInstaller.STATUS_FAILURE
)
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
?.let(logger::trace)
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
packageInstallerStatus = pmStatus
}
} }
} }
}
init {
// TODO: detect system-initiated process death during the patching process.
ContextCompat.registerReceiver(
app,
installerBroadcastReceiver,
IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION)
},
ContextCompat.RECEIVER_NOT_EXPORTED
)
viewModelScope.launch { viewModelScope.launch {
installedApp = installedAppRepository.get(packageName) installedApp = installedAppRepository.get(packageName)
@@ -295,6 +313,7 @@ class PatcherViewModel(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId.uuid) workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
@@ -309,7 +328,6 @@ class PatcherViewModel(
} }
fun onBack() { fun onBack() {
installerCoroutineScope.cancel()
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. // tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
tempDir.deleteRecursively() tempDir.deleteRecursively()
} }
@@ -354,111 +372,50 @@ class PatcherViewModel(
fun open() = installedPackageName?.let(pm::launch) fun open() = installedPackageName?.let(pm::launch)
private suspend fun startInstallation(file: File, packageName: String) {
val session = withContext(Dispatchers.IO) {
ackpineInstaller.createSession(Uri.fromFile(file)) {
confirmation = Confirmation.IMMEDIATE
}
}
withContext(Dispatchers.Main) {
installerPkgName = packageName
}
awaitInstallation(session)
}
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
Dispatchers.Main
) {
val result = installerCoroutineScope.async {
try {
installerSessionId = ParcelUuid(session.id)
withContext(Dispatchers.IO) {
session.await()
}
} finally {
installerSessionId = null
}
}.await()
when (result) {
is Session.State.Failed<InstallFailure> -> {
result.failure.message?.let(logger::trace)
packageInstallerStatus = result.failure.asCode()
}
Session.State.Succeeded -> {
app.toast(app.getString(R.string.install_app_success))
installedPackageName = installerPkgName
installedAppRepository.addOrUpdate(
installerPkgName,
packageName,
input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT,
input.selectedPatches
)
}
}
}
fun install(installType: InstallType) = viewModelScope.launch { fun install(installType: InstallType) = viewModelScope.launch {
isInstalling = true var pmInstallStarted = false
var needsRootUninstall = false
try { try {
uiSafe(app, R.string.install_app_fail, "Failed to install") { isInstalling = true
val currentPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) } when (installType) {
InstallType.DEFAULT -> {
val currentPackageInfo = pm.getPackageInfo(outputFile)
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
// If the app is currently installed // If the app is currently installed
val existingPackageInfo = val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) } if (existingPackageInfo != null) {
if (existingPackageInfo != null) { // Check if the app version is less than the installed version
// Check if the app version is less than the installed version if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
if ( // Exit if the selected app version is less than the installed version
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode( packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
existingPackageInfo return@launch
) }
) {
// 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)) {
rootInstaller.unmount(packageName)
}
// Install regularly
pm.installApp(listOf(outputFile))
pmInstallStarted = true
} }
when (installType) { InstallType.MOUNT -> {
InstallType.DEFAULT -> { try {
// Check if the app is mounted as root val packageInfo = pm.getPackageInfo(outputFile)
// If it is, unmount it first, silently ?: throw Exception("Failed to load application info")
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
rootInstaller.unmount(packageName)
}
// Install regularly
startInstallation(outputFile, currentPackageInfo.packageName)
}
InstallType.MOUNT -> {
val label = with(pm) { val label = with(pm) {
currentPackageInfo.label() packageInfo.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 val inputVersion = input.selectedApp.version
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: inputFile?.let(pm::getPackageInfo)?.versionName
?: throw Exception("Failed to determine input APK version") ?: throw Exception("Failed to determine input APK version")
needsRootUninstall = true
// Install as root // Install as root
rootInstaller.install( rootInstaller.install(
outputFile, outputFile,
@@ -469,7 +426,7 @@ class PatcherViewModel(
) )
installedAppRepository.addOrUpdate( installedAppRepository.addOrUpdate(
currentPackageInfo.packageName, packageInfo.packageName,
packageName, packageName,
inputVersion, inputVersion,
InstallType.MOUNT, InstallType.MOUNT,
@@ -481,20 +438,21 @@ class PatcherViewModel(
installedPackageName = packageName installedPackageName = packageName
app.toast(app.getString(R.string.install_app_success)) app.toast(app.getString(R.string.install_app_success))
needsRootUninstall = false } catch (e: Exception) {
Log.e(tag, "Failed to install as root", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
try {
rootInstaller.uninstall(packageName)
} catch (_: Exception) {
}
} }
} }
} }
} catch (e: Exception) {
Log.e(tag, "Failed to install", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally { } finally {
isInstalling = false if (!pmInstallStarted) isInstalling = false
if (needsRootUninstall) {
try {
withContext(NonCancellable) {
rootInstaller.uninstall(packageName)
}
} catch (_: Exception) {
}
}
} }
} }
@@ -505,27 +463,12 @@ class PatcherViewModel(
override fun reinstall() { override fun reinstall() {
viewModelScope.launch { viewModelScope.launch {
try { uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
?: throw Exception("Failed to load application info")
pm.installApp(listOf(outputFile))
isInstalling = true isInstalling = true
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
val pkgName = withContext(Dispatchers.IO) {
pm.getPackageInfo(outputFile)?.packageName
?: throw Exception("Failed to load application info")
}
when (val result = pm.uninstallPackage(pkgName)) {
is Session.State.Failed<UninstallFailure> -> {
result.failure.message?.let(logger::trace)
packageInstallerStatus = result.failure.asCode()
return@launch
}
Session.State.Succeeded -> {}
}
startInstallation(outputFile, pkgName)
}
} finally {
isInstalling = false
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import android.os.Parcelable
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -128,6 +129,8 @@ class SelectedAppInfoViewModel(
} }
var options: Options by savedStateHandle.saveable { var options: Options by savedStateHandle.saveable {
val state = mutableStateOf<Options>(emptyMap())
viewModelScope.launch { viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps. if (!persistConfiguration) return@launch // TODO: save options for patched apps.
val bundlePatches = bundleInfoFlow.first() val bundlePatches = bundleInfoFlow.first()
@@ -138,7 +141,7 @@ class SelectedAppInfoViewModel(
} }
} }
mutableStateOf(emptyMap()) state
} }
private set private set
@@ -146,6 +149,8 @@ class SelectedAppInfoViewModel(
if (input.patches != null) if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches)) return@saveable mutableStateOf(SelectionState.Customized(input.patches))
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
// Try to get the previous selection if customization is enabled. // Try to get the previous selection if customization is enabled.
viewModelScope.launch { viewModelScope.launch {
if (!prefs.disableSelectionWarning.get()) return@launch if (!prefs.disableSelectionWarning.get()) return@launch
@@ -155,7 +160,7 @@ class SelectedAppInfoViewModel(
selectionState = SelectionState.Customized(previous) selectionState = SelectionState.Customized(previous)
} }
mutableStateOf(SelectionState.Default) selection
} }
var showSourceSelector by mutableStateOf(false) var showSourceSelector by mutableStateOf(false)
@@ -306,7 +311,7 @@ class SelectedAppInfoViewModel(
} }
} }
enum class Error(@param:StringRes val resourceId: Int) { enum class Error(@StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available) NoPlugins(R.string.downloader_no_plugins_available)
} }

View File

@@ -1,13 +1,18 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.net.Uri import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@@ -16,6 +21,8 @@ import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.service.HttpService
import app.revanced.manager.service.InstallService
import app.revanced.manager.util.PM
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload import io.ktor.client.plugins.onDownload
@@ -24,14 +31,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject import org.koin.core.component.inject
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.installer.PackageInstaller
import ru.solrudev.ackpine.installer.createSession
import ru.solrudev.ackpine.session.Session
import ru.solrudev.ackpine.session.await
import ru.solrudev.ackpine.session.parameters.Confirmation
class UpdateViewModel( class UpdateViewModel(
private val downloadOnScreenEntry: Boolean private val downloadOnScreenEntry: Boolean
@@ -39,11 +39,10 @@ class UpdateViewModel(
private val app: Application by inject() private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject() private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject() private val http: HttpService by inject()
private val pm: PM by inject()
private val networkInfo: NetworkInfo by inject() private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
private val ackpineInstaller: PackageInstaller = get()
// TODO: save state to handle process death.
var downloadedSize by mutableLongStateOf(0L) var downloadedSize by mutableLongStateOf(0L)
private set private set
var totalSize by mutableLongStateOf(0L) var totalSize by mutableLongStateOf(0L)
@@ -63,17 +62,14 @@ class UpdateViewModel(
private set private set
private val location = fs.tempDir.resolve("updater.apk") private val location = fs.tempDir.resolve("updater.apk")
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
init { if (downloadOnScreenEntry) {
viewModelScope.launch { downloadUpdate()
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") { } else {
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available") state = State.CAN_DOWNLOAD
if (downloadOnScreenEntry) {
downloadUpdate()
} else {
state = State.CAN_DOWNLOAD
}
} }
} }
} }
@@ -102,36 +98,50 @@ class UpdateViewModel(
fun installUpdate() = viewModelScope.launch { fun installUpdate() = viewModelScope.launch {
state = State.INSTALLING state = State.INSTALLING
val result = withContext(Dispatchers.IO) {
ackpineInstaller.createSession(Uri.fromFile(location)) {
confirmation = Confirmation.IMMEDIATE
}.await()
}
when (result) { pm.installApp(listOf(location))
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) { }
is InstallFailure.Aborted -> state = State.CAN_INSTALL
else -> { private val installBroadcastReceiver = object : BroadcastReceiver() {
val msg = failure.message.orEmpty() override fun onReceive(context: Context?, intent: Intent?) {
app.toast(app.getString(R.string.install_app_fail, msg)) intent?.let {
installError = msg val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
state = State.FAILED val extra =
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
when(pmStatus) {
PackageInstaller.STATUS_SUCCESS -> {
app.toast(app.getString(R.string.install_app_success))
state = State.SUCCESS
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
state = State.CAN_INSTALL
}
else -> {
app.toast(app.getString(R.string.install_app_fail, extra))
installError = extra
state = State.FAILED
}
} }
} }
Session.State.Succeeded -> {
app.toast(app.getString(R.string.install_app_success))
state = State.SUCCESS
}
} }
} }
init {
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver)
job.cancel()
location.delete() location.delete()
} }
enum class State(@param:StringRes val title: Int) { enum class State(@StringRes val title: Int) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update), DOWNLOADING(R.string.downloading_manager_update),
CAN_INSTALL(R.string.ready_to_install_update), CAN_INSTALL(R.string.ready_to_install_update),

View File

@@ -1,30 +0,0 @@
package app.revanced.manager.util
import android.annotation.SuppressLint
import android.content.pm.PackageInstaller
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.uninstaller.UninstallFailure
/**
* Converts an Ackpine installation failure into a PM status code
*/
fun InstallFailure.asCode() = when (this) {
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
else -> PackageInstaller.STATUS_FAILURE
}
/**
* Converts an Ackpine uninstallation failure into a PM status code
*/
fun UninstallFailure.asCode() = when (this) {
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
else -> PackageInstaller.STATUS_FAILURE
}

View File

@@ -2,8 +2,11 @@ package app.revanced.manager.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
@@ -13,6 +16,8 @@ import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -20,13 +25,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ru.solrudev.ackpine.session.await
import ru.solrudev.ackpine.session.parameters.Confirmation
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
import ru.solrudev.ackpine.uninstaller.createSession
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
import java.io.File import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
@Immutable @Immutable
@Parcelize @Parcelize
data class AppInfo( data class AppInfo(
@@ -38,8 +40,7 @@ data class AppInfo(
@SuppressLint("QueryPermissionsNeeded") @SuppressLint("QueryPermissionsNeeded")
class PM( class PM(
private val app: Application, private val app: Application,
patchBundleRepository: PatchBundleRepository, patchBundleRepository: PatchBundleRepository
private val uninstaller: PackageUninstaller
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@@ -144,11 +145,17 @@ class PM(
false false
) )
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) { suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
uninstaller.createSession(pkg) { val packageInstaller = app.packageManager.packageInstaller
confirmation = Confirmation.IMMEDIATE packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
config() apks.forEach { apk -> session.writeApk(apk) }
}.await() session.commit(app.installIntentSender)
}
}
fun uninstallPackage(pkg: String) {
val packageInstaller = app.packageManager.packageInstaller
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
} }
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
@@ -157,4 +164,44 @@ class PM(
} }
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls() fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, byteArraySize)
fsync(outputStream)
}
}
}
private val intentFlags
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_MUTABLE
else
0
private val sessionParams
get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
setRequestUpdateOwnership(true)
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
private val Context.installIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, InstallService::class.java),
intentFlags
).intentSender
private val Context.uninstallIntentSender
get() = PendingIntent.getService(
this,
0,
Intent(this, UninstallService::class.java),
intentFlags
).intentSender
} }

View File

@@ -33,7 +33,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -83,8 +82,6 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) { inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
try { try {
block() block()
} catch (e: CancellationException) {
throw e
} catch (error: Exception) { } catch (error: Exception) {
// You can only toast on the main thread. // You can only toast on the main thread.
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {

View File

@@ -37,7 +37,6 @@ kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3" hidden-api-stub = "4.3.3"
binary-compatibility-validator = "0.17.0" binary-compatibility-validator = "0.17.0"
semver-parser = "3.0.0" semver-parser = "3.0.0"
ackpine = "0.18.5"
[libraries] [libraries]
# AndroidX Core # AndroidX Core
@@ -134,10 +133,6 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
# Semantic versioning parser # Semantic versioning parser
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" } semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
# Ackpine
ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }