mirror of
https://github.com/revanced/revanced-manager-compose-old.git
synced 2025-04-30 06:24:28 +02:00
feat: app installation (#15)
This commit is contained in:
parent
0adf6c1eaa
commit
cf49b49494
@ -50,6 +50,8 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
|
||||||
|
|
||||||
|
<service android:name=".installer.service.InstallService" />
|
||||||
|
<service android:name=".installer.service.UninstallService" />
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -0,0 +1,47 @@
|
|||||||
|
package app.revanced.manager.installer.service
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||||
|
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package app.revanced.manager.installer.service
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
class UninstallService : Service() {
|
||||||
|
|
||||||
|
override fun onStartCommand(
|
||||||
|
intent: Intent,
|
||||||
|
flags: Int,
|
||||||
|
startId: Int
|
||||||
|
): Int {
|
||||||
|
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
|
||||||
|
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_UNINSTALL_ACTION
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
68
app/src/main/java/app/revanced/manager/installer/utils/PM.kt
Normal file
68
app/src/main/java/app/revanced/manager/installer/utils/PM.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package app.revanced.manager.installer.utils
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import app.revanced.manager.installer.service.InstallService
|
||||||
|
import app.revanced.manager.installer.service.UninstallService
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||||
|
|
||||||
|
object PM {
|
||||||
|
|
||||||
|
fun installApp(apk: File, context: Context) {
|
||||||
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
|
val session =
|
||||||
|
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||||
|
session.writeApk(apk)
|
||||||
|
session.commit(context.installIntentSender)
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallPackage(pkg: String, context: Context) {
|
||||||
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
|
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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
|
@ -0,0 +1,48 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InstallFailureDialog(
|
||||||
|
onDismiss: () -> Unit, status: Int, result: String
|
||||||
|
) {
|
||||||
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val reason = when (status) {
|
||||||
|
PackageInstaller.STATUS_FAILURE_BLOCKED -> stringResource(R.string.status_failure_blocked)
|
||||||
|
PackageInstaller.STATUS_FAILURE_ABORTED -> stringResource(R.string.status_failure_aborted)
|
||||||
|
PackageInstaller.STATUS_FAILURE_CONFLICT -> stringResource(R.string.status_failure_conflict)
|
||||||
|
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> stringResource(R.string.status_failure_incompatible)
|
||||||
|
PackageInstaller.STATUS_FAILURE_INVALID -> stringResource(R.string.status_failure_invalid)
|
||||||
|
PackageInstaller.STATUS_FAILURE_STORAGE -> stringResource(R.string.status_failure_storage)
|
||||||
|
else -> stringResource(R.string.status_failure)
|
||||||
|
}
|
||||||
|
if (showDetails) {
|
||||||
|
AlertDialog(onDismissRequest = onDismiss, confirmButton = {
|
||||||
|
Button(onClick = { showDetails = false }) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}, title = { Text(stringResource(R.string.details)) }, text = {
|
||||||
|
Text(result)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
AlertDialog(onDismissRequest = onDismiss, confirmButton = {
|
||||||
|
Button(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}, dismissButton = {
|
||||||
|
OutlinedButton(onClick = { showDetails = true }) {
|
||||||
|
Text(stringResource(R.string.details))
|
||||||
|
}
|
||||||
|
}, title = { Text(stringResource(R.string.install)) }, text = {
|
||||||
|
Text(reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package app.revanced.manager.ui.screen.subscreens
|
package app.revanced.manager.ui.screen.subscreens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
@ -13,11 +15,15 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.InstallFailureDialog
|
||||||
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel
|
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.PatchLog
|
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.PatchLog
|
||||||
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.Status
|
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.Status
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
|
||||||
@ -27,14 +33,10 @@ import org.koin.androidx.compose.getViewModel
|
|||||||
fun PatchingSubscreen(
|
fun PatchingSubscreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatchingScreenViewModel = getViewModel()
|
vm: PatchingScreenViewModel = getViewModel()
|
||||||
|
|
||||||
) {
|
) {
|
||||||
var patching by mutableStateOf(false)
|
val scrollState = rememberScrollState()
|
||||||
LaunchedEffect(patching) {
|
val coroutineScope = rememberCoroutineScope()
|
||||||
if (!patching) {
|
|
||||||
patching = true
|
|
||||||
vm.startPatcher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -49,6 +51,13 @@ fun PatchingSubscreen(
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column {
|
Column {
|
||||||
|
if (vm.installFailure) {
|
||||||
|
InstallFailureDialog(
|
||||||
|
onDismiss = { vm.installFailure = false },
|
||||||
|
status = vm.pmStatus,
|
||||||
|
result = vm.extra
|
||||||
|
)
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@ -62,7 +71,6 @@ fun PatchingSubscreen(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
|
||||||
when (vm.status) {
|
when (vm.status) {
|
||||||
is Status.Failure -> {
|
is Status.Failure -> {
|
||||||
Icon(
|
Icon(
|
||||||
@ -72,7 +80,7 @@ fun PatchingSubscreen(
|
|||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
)
|
)
|
||||||
Text(text = "Failed!", fontSize = 30.sp)
|
Text(text = stringResource(R.string.failed), fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
is Status.Patching -> {
|
is Status.Patching -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
@ -80,7 +88,7 @@ fun PatchingSubscreen(
|
|||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
)
|
)
|
||||||
Text(text = "Patching...", fontSize = 30.sp)
|
Text(text = stringResource(R.string.patching), fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
is Status.Success -> {
|
is Status.Success -> {
|
||||||
Icon(
|
Icon(
|
||||||
@ -90,7 +98,7 @@ fun PatchingSubscreen(
|
|||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
)
|
)
|
||||||
Text(text = "Completed!", fontSize = 30.sp)
|
Text(text = stringResource(R.string.completed), fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
Status.Idle -> {}
|
Status.Idle -> {}
|
||||||
}
|
}
|
||||||
@ -98,18 +106,23 @@ fun PatchingSubscreen(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxHeight()
|
||||||
.padding(20.dp)
|
.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
ElevatedCard {
|
ElevatedCard(
|
||||||
LazyColumn(
|
Modifier
|
||||||
|
.weight(1f, true)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 20.dp, vertical = 10.dp)
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
items(vm.logs) { log ->
|
vm.logs.forEach { log ->
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.height(36.dp),
|
modifier = Modifier.requiredHeightIn(min = 36.dp),
|
||||||
text = log.message,
|
text = log.message,
|
||||||
color = when (log) {
|
color = when (log) {
|
||||||
is PatchLog.Success -> Color.Green
|
is PatchLog.Success -> Color.Green
|
||||||
@ -117,9 +130,30 @@ fun PatchingSubscreen(
|
|||||||
is PatchLog.Error -> Color.Red
|
is PatchLog.Error -> Color.Red
|
||||||
},
|
},
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
|
onTextLayout = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
scrollState.animateScrollTo(
|
||||||
|
9999, tween(1000, easing = LinearEasing)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vm.status is Status.Success) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.weight(1f, true))
|
||||||
|
Button(onClick = {
|
||||||
|
vm.installApk(vm.outputFile)
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.install))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
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.IntentFilter
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.installer.service.InstallService
|
||||||
|
import app.revanced.manager.installer.service.UninstallService
|
||||||
|
import app.revanced.manager.installer.utils.PM
|
||||||
import app.revanced.manager.network.api.ManagerAPI
|
import app.revanced.manager.network.api.ManagerAPI
|
||||||
import app.revanced.manager.patcher.PatcherUtils
|
import app.revanced.manager.patcher.PatcherUtils
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
@ -24,7 +32,6 @@ import app.revanced.patcher.PatcherOptions
|
|||||||
import app.revanced.patcher.logging.Logger
|
import app.revanced.patcher.logging.Logger
|
||||||
import io.sentry.Sentry
|
import io.sentry.Sentry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancelChildren
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -39,6 +46,11 @@ class PatchingScreenViewModel(
|
|||||||
private val patcherUtils: PatcherUtils
|
private val patcherUtils: PatcherUtils
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var installFailure by mutableStateOf(false)
|
||||||
|
|
||||||
|
var pmStatus by mutableStateOf(-999)
|
||||||
|
var extra by mutableStateOf("")
|
||||||
|
|
||||||
sealed interface PatchLog {
|
sealed interface PatchLog {
|
||||||
val message: String
|
val message: String
|
||||||
|
|
||||||
@ -47,9 +59,6 @@ class PatchingScreenViewModel(
|
|||||||
data class Error(override val message: String) : PatchLog
|
data class Error(override val message: String) : PatchLog
|
||||||
}
|
}
|
||||||
|
|
||||||
val logs = mutableStateListOf<PatchLog>()
|
|
||||||
var status by mutableStateOf<Status>(Status.Idle)
|
|
||||||
|
|
||||||
sealed class Status {
|
sealed class Status {
|
||||||
object Idle : Status()
|
object Idle : Status()
|
||||||
object Patching : Status()
|
object Patching : Status()
|
||||||
@ -57,55 +66,79 @@ class PatchingScreenViewModel(
|
|||||||
object Failure : Status()
|
object Failure : Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPatcher() {
|
val outputFile = File(app.filesDir, "output.apk")
|
||||||
cancelPatching() // cancel patching if its still running
|
val logs = mutableStateListOf<PatchLog>()
|
||||||
logs.clear()
|
var status by mutableStateOf<Status>(Status.Idle)
|
||||||
|
|
||||||
|
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
InstallService.APP_INSTALL_ACTION -> {
|
||||||
|
pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||||
|
extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||||
|
postInstallStatus()
|
||||||
|
}
|
||||||
|
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
status = Status.Patching
|
status = Status.Patching
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
app.registerReceiver(
|
||||||
try {
|
installBroadcastReceiver,
|
||||||
runPatcher(createWorkDir())
|
IntentFilter().apply {
|
||||||
} catch (e: Exception) {
|
addAction(InstallService.APP_INSTALL_ACTION)
|
||||||
status = Status.Failure
|
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||||
Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
|
|
||||||
Sentry.captureException(e)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installApk(apk: File) {
|
||||||
|
PM.installApp(apk, app)
|
||||||
|
log(PatchLog.Info("Installing..."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postInstallStatus() {
|
||||||
|
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
|
log(PatchLog.Success("Successfully installed!"))
|
||||||
|
} else {
|
||||||
|
installFailure = true
|
||||||
|
log(PatchLog.Error("Failed to install!"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPatching() {
|
private val patcher = viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.coroutineContext.cancelChildren(CancellationException("Patching was cancelled by user."))
|
val workdir = createWorkDir()
|
||||||
logs.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun runPatcher(
|
|
||||||
workdir: File
|
|
||||||
) {
|
|
||||||
val wakeLock: PowerManager.WakeLock =
|
val wakeLock: PowerManager.WakeLock =
|
||||||
(app.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(app.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.FULL_WAKE_LOCK, "$tag::Patcher").apply {
|
newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
|
||||||
acquire(10 * 60 * 1000L)
|
acquire(10 * 60 * 1000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(tag, "Acquired wakelock.")
|
Log.d(tag, "Acquired wakelock.")
|
||||||
|
try {
|
||||||
val aaptPath = Aapt.binary(app)?.absolutePath
|
val aaptPath = Aapt.binary(app)?.absolutePath
|
||||||
if (aaptPath == null) {
|
if (aaptPath == null) {
|
||||||
log(PatchLog.Error("AAPT2 not found."))
|
log(PatchLog.Error("AAPT2 not found."))
|
||||||
throw FileNotFoundException()
|
throw FileNotFoundException()
|
||||||
}
|
}
|
||||||
val frameworkPath = app.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
val frameworkPath = app.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
|
||||||
val integrationsCacheDir = app.filesDir.resolve("integrations-cache").also { it.mkdirs() }
|
val integrationsCacheDir =
|
||||||
|
app.filesDir.resolve("integrations-cache").also { it.mkdirs() }
|
||||||
val reVancedFolder =
|
val reVancedFolder =
|
||||||
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
|
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
|
||||||
val appInfo = patcherUtils.selectedAppPackage.value.get()
|
val appInfo = patcherUtils.selectedAppPackage.value.get()
|
||||||
|
|
||||||
log(PatchLog.Info("Checking prerequisites..."))
|
log(PatchLog.Info("Checking prerequisites..."))
|
||||||
val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
|
val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
|
||||||
if (patches.isEmpty()) return
|
if (patches.isEmpty()) throw IllegalStateException("No patches selected.")
|
||||||
|
|
||||||
log(PatchLog.Info("Creating directories..."))
|
log(PatchLog.Info("Creating directories..."))
|
||||||
val inputFile = File(app.filesDir, "input.apk")
|
val inputFile = File(app.filesDir, "input.apk")
|
||||||
val patchedFile = File(workdir, "patched.apk")
|
val patchedFile = File(workdir, "patched.apk")
|
||||||
val outputFile = File(app.filesDir, "output.apk")
|
|
||||||
val cacheDirectory = workdir.resolve("cache")
|
val cacheDirectory = workdir.resolve("cache")
|
||||||
|
|
||||||
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
|
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
|
||||||
@ -118,11 +151,10 @@ class PatchingScreenViewModel(
|
|||||||
StandardCopyOption.REPLACE_EXISTING
|
StandardCopyOption.REPLACE_EXISTING
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
log(PatchLog.Info("Decoding resources"))
|
log(PatchLog.Info("Decoding resources"))
|
||||||
val patcher = Patcher( // start patcher
|
val patcher = Patcher( // start patcher
|
||||||
PatcherOptions(inputFile,
|
PatcherOptions(
|
||||||
|
inputFile,
|
||||||
cacheDirectory.absolutePath,
|
cacheDirectory.absolutePath,
|
||||||
aaptPath = aaptPath,
|
aaptPath = aaptPath,
|
||||||
frameworkFolderLocation = frameworkPath,
|
frameworkFolderLocation = frameworkPath,
|
||||||
@ -189,12 +221,22 @@ class PatchingScreenViewModel(
|
|||||||
log(PatchLog.Success("Successfully patched!"))
|
log(PatchLog.Success("Successfully patched!"))
|
||||||
patcherUtils.cleanup()
|
patcherUtils.cleanup()
|
||||||
status = Status.Success
|
status = Status.Success
|
||||||
} finally {
|
} catch (e: Exception) {
|
||||||
|
status = Status.Failure
|
||||||
|
Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
|
||||||
|
Sentry.captureException(e)
|
||||||
|
}
|
||||||
Log.d(tag, "Deleting workdir")
|
Log.d(tag, "Deleting workdir")
|
||||||
workdir.deleteRecursively()
|
workdir.deleteRecursively()
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
Log.d(tag, "Released wakelock.")
|
Log.d(tag, "Released wakelock.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
app.unregisterReceiver(installBroadcastReceiver)
|
||||||
|
patcher.cancel(CancellationException("ViewModel cleared"))
|
||||||
|
logs.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createWorkDir(): File {
|
private fun createWorkDir(): File {
|
||||||
|
@ -52,4 +52,17 @@
|
|||||||
<string name="dismiss">Dismiss</string>
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="patch">Patch</string>
|
<string name="patch">Patch</string>
|
||||||
<string name="no_compatible_patches">No compatible patches found.</string>
|
<string name="no_compatible_patches">No compatible patches found.</string>
|
||||||
|
<string name="status_failure_blocked">Installation failed due to installer being blocked by the operating system.</string>
|
||||||
|
<string name="status_failure">Installation failed due to an unknown error.</string>
|
||||||
|
<string name="status_failure_conflict">Installation failed due to conflicting packages. Please uninstall the conflicting app before retrying installation.</string>
|
||||||
|
<string name="status_failure_aborted">Installation failed due to user abortion.</string>
|
||||||
|
<string name="status_failure_incompatible">Installation failed due to the app you\'re trying to install being incompatible with current device.</string>
|
||||||
|
<string name="status_failure_invalid">Installation failed due to package being invalid. This might be caused by unsigned or corrupted package.</string>
|
||||||
|
<string name="status_failure_storage">Installation failed due to insufficient storage.</string>
|
||||||
|
<string name="details">Details</string>
|
||||||
|
<string name="install">Install</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="patching">Patching…</string>
|
||||||
|
<string name="completed">Completed!</string>
|
||||||
|
<string name="failed">Failed!</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user