mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: root installation (#1243)
This commit is contained in:
parent
b4dfcf1bb4
commit
bf10af2ae2
@ -66,6 +66,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures.compose = true
|
buildFeatures.compose = true
|
||||||
|
buildFeatures.aidl = true
|
||||||
|
|
||||||
composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
|
composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
|
||||||
}
|
}
|
||||||
@ -124,6 +125,10 @@ dependencies {
|
|||||||
implementation(libs.apksign)
|
implementation(libs.apksign)
|
||||||
implementation(libs.bcpkix.jdk18on)
|
implementation(libs.bcpkix.jdk18on)
|
||||||
|
|
||||||
|
implementation(libs.libsu.core)
|
||||||
|
implementation(libs.libsu.service)
|
||||||
|
implementation(libs.libsu.nio)
|
||||||
|
|
||||||
// Koin
|
// Koin
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// IRootService.aidl
|
||||||
|
package app.revanced.manager;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootSystemService {
|
||||||
|
IBinder getFileSystemService();
|
||||||
|
}
|
6
app/src/main/assets/root/module.prop
Normal file
6
app/src/main/assets/root/module.prop
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
id=__PKG_NAME__-ReVanced
|
||||||
|
name=__LABEL__ ReVanced
|
||||||
|
version=__VERSION__
|
||||||
|
versionCode=0
|
||||||
|
author=ReVanced
|
||||||
|
description=Mounts the patched apk on top of the original apk
|
40
app/src/main/assets/root/service.sh
Normal file
40
app/src/main/assets/root/service.sh
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
DIR=${0%/*}
|
||||||
|
|
||||||
|
package_name="__PKG_NAME__"
|
||||||
|
version="__VERSION__"
|
||||||
|
|
||||||
|
rm "$DIR/log"
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
base_path="$DIR/$package_name.apk"
|
||||||
|
stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')"
|
||||||
|
stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)"
|
||||||
|
|
||||||
|
echo "base_path: $base_path"
|
||||||
|
echo "stock_path: $stock_path"
|
||||||
|
echo "base_version: $version"
|
||||||
|
echo "stock_version: $stock_version"
|
||||||
|
|
||||||
|
if mount | grep -q "$stock_path" ; then
|
||||||
|
echo "Not mounting as stock path is already mounted"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$version" != "$stock_version" ]; then
|
||||||
|
echo "Not mounting as versions don't match"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$stock_path" ]; then
|
||||||
|
echo "Not mounting as app info could not be loaded"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mount -o bind "$base_path" "$stock_path"
|
||||||
|
|
||||||
|
} >> "$DIR/log"
|
@ -34,10 +34,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val vm: MainViewModel = getActivityViewModel()
|
|
||||||
|
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
|
val vm: MainViewModel = getActivityViewModel()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by vm.prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
package app.revanced.manager
|
package app.revanced.manager
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
import app.revanced.manager.di.*
|
import app.revanced.manager.di.*
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.service.ManagerRootService
|
||||||
|
import app.revanced.manager.service.RootConnection
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.BuilderImpl
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
@ -37,6 +44,7 @@ class ManagerApplication : Application() {
|
|||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
databaseModule,
|
databaseModule,
|
||||||
|
rootModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +58,12 @@ class ManagerApplication : Application() {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
|
Shell.setDefaultBuilder(shellBuilder)
|
||||||
|
|
||||||
|
val intent = Intent(this, ManagerRootService::class.java)
|
||||||
|
RootService.bind(intent, get<RootConnection>())
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
prefs.preload()
|
prefs.preload()
|
||||||
}
|
}
|
||||||
|
11
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
11
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
|
import app.revanced.manager.service.RootConnection
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val rootModule = module {
|
||||||
|
singleOf(::RootConnection)
|
||||||
|
singleOf(::RootInstaller)
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
package app.revanced.manager.domain.installer
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import app.revanced.manager.service.RootConnection
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class RootInstaller(
|
||||||
|
private val app: Application,
|
||||||
|
private val rootConnection: RootConnection,
|
||||||
|
private val pm: PM
|
||||||
|
) {
|
||||||
|
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||||
|
|
||||||
|
fun isAppInstalled(packageName: String) =
|
||||||
|
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced")
|
||||||
|
?.exists() ?: throw RootServiceException()
|
||||||
|
|
||||||
|
fun isAppMounted(packageName: String): Boolean {
|
||||||
|
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
|
||||||
|
Shell.cmd("mount | grep \"$it\"").exec().isSuccess
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mount(packageName: String) {
|
||||||
|
if (isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||||
|
|
||||||
|
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec()
|
||||||
|
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unmount(packageName: String) {
|
||||||
|
if (!isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
Shell.cmd("umount -l \"$stockAPK\"").exec()
|
||||||
|
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun install(
|
||||||
|
patchedAPK: File,
|
||||||
|
stockAPK: File?,
|
||||||
|
packageName: String,
|
||||||
|
version: String,
|
||||||
|
label: String
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
rootConnection.remoteFS?.let { remoteFS ->
|
||||||
|
val assets = app.assets
|
||||||
|
val modulePath = "$modulesPath/$packageName-revanced"
|
||||||
|
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
stockAPK?.let { stockApp ->
|
||||||
|
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||||
|
if (packageInfo.versionName <= version)
|
||||||
|
Shell.cmd("pm uninstall -k --user 0 $packageName").exec()
|
||||||
|
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
|
||||||
|
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteFS.getFile(modulePath).mkdir()
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
"service.sh",
|
||||||
|
"module.prop",
|
||||||
|
).forEach { file ->
|
||||||
|
assets.open("root/$file").use { inputStream ->
|
||||||
|
remoteFS.getFile("$modulePath/$file").newOutputStream()
|
||||||
|
.use { outputStream ->
|
||||||
|
val content = String(inputStream.readBytes())
|
||||||
|
.replace("__PKG_NAME__", packageName)
|
||||||
|
.replace("__VERSION__", version)
|
||||||
|
.replace("__LABEL__", label)
|
||||||
|
.toByteArray()
|
||||||
|
|
||||||
|
outputStream.write(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"$modulePath/$packageName.apk".let { apkPath ->
|
||||||
|
|
||||||
|
remoteFS.getFile(patchedAPK.absolutePath)
|
||||||
|
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||||
|
.newInputStream().use { inputStream ->
|
||||||
|
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shell.cmd(
|
||||||
|
"chmod 644 $apkPath",
|
||||||
|
"chown system:system $apkPath",
|
||||||
|
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||||
|
"chmod +x $modulePath/service.sh"
|
||||||
|
).exec()
|
||||||
|
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
|
||||||
|
}
|
||||||
|
} ?: throw RootServiceException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstall(packageName: String) {
|
||||||
|
rootConnection.remoteFS?.let { remoteFS ->
|
||||||
|
if (isAppMounted(packageName))
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
|
||||||
|
.also { if (!it) throw Exception("Failed to delete files") }
|
||||||
|
} ?: throw RootServiceException()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val modulesPath = "/data/adb/modules"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootServiceException: Exception("Root not available")
|
@ -14,8 +14,11 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
@ -29,7 +32,6 @@ import app.revanced.manager.util.PatchesSelection
|
|||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.options
|
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -49,6 +51,8 @@ class PatcherWorker(
|
|||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
data class Args(
|
data class Args(
|
||||||
val input: SelectedApp,
|
val input: SelectedApp,
|
||||||
@ -58,7 +62,9 @@ class PatcherWorker(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val packageVersion: String,
|
val packageVersion: String,
|
||||||
val progress: MutableStateFlow<ImmutableList<Step>>,
|
val progress: MutableStateFlow<ImmutableList<Step>>,
|
||||||
val logger: ManagerLogger
|
val logger: ManagerLogger,
|
||||||
|
val selectedApp: SelectedApp,
|
||||||
|
val setInputFile: (File) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -148,6 +154,15 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
|
||||||
|
if (args.selectedApp is SelectedApp.Installed) {
|
||||||
|
installedAppRepository.get(args.packageName)?.let {
|
||||||
|
if (it.installType == InstallType.ROOT) {
|
||||||
|
rootInstaller.unmount(args.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||||
val selectedBundles = args.selectedPatches.keys
|
val selectedBundles = args.selectedPatches.keys
|
||||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||||
@ -190,11 +205,12 @@ class PatcherWorker(
|
|||||||
args.input.version,
|
args.input.version,
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
|
args.setInputFile(it)
|
||||||
updateProgress() // Downloading
|
updateProgress() // Downloading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file
|
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package app.revanced.manager.service
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import app.revanced.manager.IRootSystemService
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
|
|
||||||
|
class ManagerRootService : RootService() {
|
||||||
|
class RootSystemService : IRootSystemService.Stub() {
|
||||||
|
override fun getFileSystemService() =
|
||||||
|
FileSystemManager.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
return RootSystemService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootConnection : ServiceConnection {
|
||||||
|
var remoteFS: FileSystemManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val ipc = IRootSystemService.Stub.asInterface(service)
|
||||||
|
val binder = ipc.fileSystemService
|
||||||
|
|
||||||
|
remoteFS = FileSystemManager.getRemote(binder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
remoteFS = null
|
||||||
|
}
|
||||||
|
}
|
@ -9,10 +9,12 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
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.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
@ -26,16 +28,28 @@ import androidx.compose.ui.unit.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun RowScope.SegmentedButton(
|
fun RowScope.SegmentedButton(
|
||||||
icon: Any,
|
icon: Any,
|
||||||
iconDescription: String? = null,
|
|
||||||
text: String,
|
text: String,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
iconDescription: String? = null,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
|
val contentColor = if (enabled)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(0.38f)
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = onClick)
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
|
.background(
|
||||||
|
if (enabled)
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(0.12f)
|
||||||
|
)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(vertical = 20.dp)
|
.padding(vertical = 20.dp)
|
||||||
) {
|
) {
|
||||||
@ -43,16 +57,14 @@ fun RowScope.SegmentedButton(
|
|||||||
is ImageVector -> {
|
is ImageVector -> {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = iconDescription,
|
contentDescription = iconDescription
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Painter -> {
|
is Painter -> {
|
||||||
Icon(
|
Icon(
|
||||||
painter = icon,
|
painter = icon,
|
||||||
contentDescription = iconDescription,
|
contentDescription = iconDescription
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,9 +72,9 @@ fun RowScope.SegmentedButton(
|
|||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier.basicMarquee()
|
modifier = Modifier.basicMarquee()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
@ -13,17 +13,25 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowRight
|
import androidx.compose.material.icons.filled.ArrowRight
|
||||||
|
import androidx.compose.material.icons.outlined.Circle
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.OpenInNew
|
import androidx.compose.material.icons.outlined.OpenInNew
|
||||||
|
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -31,6 +39,7 @@ import androidx.compose.ui.res.pluralStringResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.ui.component.AppIcon
|
import app.revanced.manager.ui.component.AppIcon
|
||||||
import app.revanced.manager.ui.component.AppLabel
|
import app.revanced.manager.ui.component.AppLabel
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
@ -49,6 +58,14 @@ fun AppInfoScreen(
|
|||||||
viewModel.onBackClick = onBackClick
|
viewModel.onBackClick = onBackClick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showUninstallDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showUninstallDialog)
|
||||||
|
UninstallDialog(
|
||||||
|
onDismiss = { showUninstallDialog = false },
|
||||||
|
onConfirm = { viewModel.uninstall() }
|
||||||
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(
|
AppTopBar(
|
||||||
@ -84,6 +101,17 @@ fun AppInfoScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
|
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
|
||||||
|
|
||||||
|
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||||
|
Text(
|
||||||
|
text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) {
|
||||||
|
stringResource(R.string.mounted)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.not_mounted)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -98,12 +126,31 @@ fun AppInfoScreen(
|
|||||||
onClick = viewModel::launch
|
onClick = viewModel::launch
|
||||||
)
|
)
|
||||||
|
|
||||||
SegmentedButton(
|
when (viewModel.installedApp.installType) {
|
||||||
|
InstallType.DEFAULT -> SegmentedButton(
|
||||||
icon = Icons.Outlined.Delete,
|
icon = Icons.Outlined.Delete,
|
||||||
text = stringResource(R.string.uninstall),
|
text = stringResource(R.string.uninstall),
|
||||||
onClick = viewModel::uninstall
|
onClick = viewModel::uninstall
|
||||||
)
|
)
|
||||||
|
|
||||||
|
InstallType.ROOT -> {
|
||||||
|
SegmentedButton(
|
||||||
|
icon = Icons.Outlined.SettingsBackupRestore,
|
||||||
|
text = stringResource(R.string.unpatch),
|
||||||
|
onClick = { showUninstallDialog = true },
|
||||||
|
enabled = viewModel.rootInstaller.hasRootAccess()
|
||||||
|
)
|
||||||
|
|
||||||
|
SegmentedButton(
|
||||||
|
icon = Icons.Outlined.Circle,
|
||||||
|
text = if (viewModel.isMounted) stringResource(R.string.unmount) else stringResource(R.string.mount),
|
||||||
|
onClick = viewModel::mountOrUnmount,
|
||||||
|
enabled = viewModel.rootInstaller.hasRootAccess()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
icon = Icons.Outlined.Update,
|
icon = Icons.Outlined.Update,
|
||||||
text = stringResource(R.string.repatch),
|
text = stringResource(R.string.repatch),
|
||||||
@ -111,7 +158,8 @@ fun AppInfoScreen(
|
|||||||
viewModel.appliedPatches?.let {
|
viewModel.appliedPatches?.let {
|
||||||
onPatchClick(viewModel.installedApp.originalPackageName, it)
|
onPatchClick(viewModel.installedApp.originalPackageName, it)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,3 +204,30 @@ fun AppInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UninstallDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) = AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.unpatch_app)) },
|
||||||
|
text = { Text(stringResource(R.string.unpatch_description)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
@ -5,6 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@ -35,6 +36,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.patcher.worker.Step
|
import app.revanced.manager.patcher.worker.Step
|
||||||
import app.revanced.manager.patcher.worker.State
|
import app.revanced.manager.patcher.worker.State
|
||||||
import app.revanced.manager.ui.component.AppScaffold
|
import app.revanced.manager.ui.component.AppScaffold
|
||||||
@ -59,6 +61,13 @@ fun InstallerScreen(
|
|||||||
val steps by vm.progress.collectAsStateWithLifecycle()
|
val steps by vm.progress.collectAsStateWithLifecycle()
|
||||||
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||||
var dropdownActive by rememberSaveable { mutableStateOf(false) }
|
var dropdownActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showInstallPicker)
|
||||||
|
InstallPicker(
|
||||||
|
onDismiss = { showInstallPicker = false },
|
||||||
|
onConfirm = { vm.install(it) }
|
||||||
|
)
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -111,7 +120,12 @@ fun InstallerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = vm::installOrOpen,
|
onClick = {
|
||||||
|
if (vm.installedPackageName == null)
|
||||||
|
showInstallPicker = true
|
||||||
|
else
|
||||||
|
vm.open()
|
||||||
|
},
|
||||||
enabled = canInstall
|
enabled = canInstall
|
||||||
) {
|
) {
|
||||||
Text(stringResource(vm.appButtonText))
|
Text(stringResource(vm.appButtonText))
|
||||||
@ -121,6 +135,51 @@ fun InstallerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InstallPicker(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (InstallType) -> Unit
|
||||||
|
) {
|
||||||
|
var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
dismissButton = {
|
||||||
|
Button(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onConfirm(selectedInstallType)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.install_app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.select_install_type)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
InstallType.values().forEach {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { selectedInstallType = it },
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = selectedInstallType == it,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(it.stringResource)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
@ -78,7 +79,7 @@ fun VersionSelectorScreen(
|
|||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text("Select version") },
|
text = { Text(stringResource(R.string.select_version)) },
|
||||||
icon = { Icon(Icons.Default.Check, null) },
|
icon = { Icon(Icons.Default.Check, null) },
|
||||||
onClick = { selectedVersion?.let(onAppClick) }
|
onClick = { selectedVersion?.let(onAppClick) }
|
||||||
)
|
)
|
||||||
@ -90,7 +91,7 @@ fun VersionSelectorScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
|
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
||||||
SelectedApp.Installed(
|
SelectedApp.Installed(
|
||||||
packageName = viewModel.packageName,
|
packageName = viewModel.packageName,
|
||||||
version = packageInfo.versionName
|
version = packageInfo.versionName
|
||||||
@ -100,12 +101,14 @@ fun VersionSelectorScreen(
|
|||||||
selected = selectedVersion == it,
|
selected = selectedVersion == it,
|
||||||
onClick = { selectedVersion = it },
|
onClick = { selectedVersion = it },
|
||||||
patchCount = supportedVersions[it.version],
|
patchCount = supportedVersions[it.version],
|
||||||
alreadyPatched = alreadyPatched
|
enabled =
|
||||||
|
!(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
|
||||||
|
alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupHeader("Downloadable versions")
|
GroupHeader(stringResource(R.string.downloadable_versions))
|
||||||
|
|
||||||
list.forEach {
|
list.forEach {
|
||||||
SelectedAppItem(
|
SelectedAppItem(
|
||||||
@ -140,6 +143,7 @@ fun SelectedAppItem(
|
|||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
patchCount: Int?,
|
patchCount: Int?,
|
||||||
|
enabled: Boolean = true,
|
||||||
alreadyPatched: Boolean = false
|
alreadyPatched: Boolean = false
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@ -148,9 +152,9 @@ fun SelectedAppItem(
|
|||||||
supportingContent = when (selectedApp) {
|
supportingContent = when (selectedApp) {
|
||||||
is SelectedApp.Installed ->
|
is SelectedApp.Installed ->
|
||||||
if (alreadyPatched) {
|
if (alreadyPatched) {
|
||||||
{ Text("Already patched") }
|
{ Text(stringResource(R.string.already_patched)) }
|
||||||
} else {
|
} else {
|
||||||
{ Text("Installed") }
|
{ Text(stringResource(R.string.installed)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> {
|
is SelectedApp.Local -> {
|
||||||
@ -163,9 +167,9 @@ fun SelectedAppItem(
|
|||||||
Text(pluralStringResource(R.plurals.patches_count, it, it))
|
Text(pluralStringResource(R.plurals.patches_count, it, it))
|
||||||
} },
|
} },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(enabled = !alreadyPatched, onClick = onClick)
|
.clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
|
||||||
.run {
|
.run {
|
||||||
if (alreadyPatched) alpha(0.5f)
|
if (!enabled || alreadyPatched) alpha(0.5f)
|
||||||
else this
|
else this
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
|
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
|
||||||
@ -15,10 +16,13 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
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.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.service.UninstallService
|
import app.revanced.manager.service.UninstallService
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -32,20 +36,46 @@ class AppInfoViewModel(
|
|||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
|
val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
lateinit var onBackClick: () -> Unit
|
lateinit var onBackClick: () -> Unit
|
||||||
|
|
||||||
var appInfo: PackageInfo? by mutableStateOf(null)
|
var appInfo: PackageInfo? by mutableStateOf(null)
|
||||||
private set
|
private set
|
||||||
var appliedPatches: PatchesSelection? by mutableStateOf(null)
|
var appliedPatches: PatchesSelection? by mutableStateOf(null)
|
||||||
|
var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName))
|
||||||
|
private set
|
||||||
|
|
||||||
fun launch() = pm.launch(installedApp.currentPackageName)
|
fun launch() = pm.launch(installedApp.currentPackageName)
|
||||||
|
|
||||||
|
fun mountOrUnmount() {
|
||||||
|
try {
|
||||||
|
if (isMounted)
|
||||||
|
rootInstaller.unmount(installedApp.currentPackageName)
|
||||||
|
else
|
||||||
|
rootInstaller.mount(installedApp.currentPackageName)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (isMounted) {
|
||||||
|
app.toast(app.getString(R.string.failed_to_unmount, e.simpleMessage()))
|
||||||
|
Log.e(tag, "Failed to unmount", e)
|
||||||
|
} else {
|
||||||
|
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||||
|
Log.e(tag, "Failed to mount", e)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun uninstall() {
|
fun uninstall() {
|
||||||
when (installedApp.installType) {
|
when (installedApp.installType) {
|
||||||
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
|
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
|
||||||
|
|
||||||
InstallType.ROOT -> TODO()
|
InstallType.ROOT -> viewModelScope.launch {
|
||||||
|
rootInstaller.uninstall(installedApp.currentPackageName)
|
||||||
|
installedAppRepository.delete(installedApp)
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +89,7 @@ class AppInfoViewModel(
|
|||||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedAppRepository.delete(installedApp)
|
installedAppRepository.delete(installedApp)
|
||||||
withContext(Dispatchers.Main) { onBackClick() }
|
onBackClick()
|
||||||
}
|
}
|
||||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||||
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||||
|
@ -11,22 +11,19 @@ import java.nio.file.Files
|
|||||||
|
|
||||||
class AppSelectorViewModel(
|
class AppSelectorViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
pm: PM
|
private val pm: PM
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val packageManager = app.packageManager
|
|
||||||
|
|
||||||
val appList = pm.appList
|
val appList = pm.appList
|
||||||
|
|
||||||
fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString()
|
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun loadSelectedFile(uri: Uri) =
|
fun loadSelectedFile(uri: Uri) =
|
||||||
app.contentResolver.openInputStream(uri)?.use { stream ->
|
app.contentResolver.openInputStream(uri)?.use { stream ->
|
||||||
File(app.cacheDir, "input.apk").also {
|
File(app.cacheDir, "input.apk").also {
|
||||||
it.delete()
|
it.delete()
|
||||||
Files.copy(stream, it.toPath())
|
Files.copy(stream, it.toPath())
|
||||||
}.let { file ->
|
}.let { file ->
|
||||||
packageManager.getPackageArchiveInfo(file.absolutePath, 0)
|
pm.getPackageInfo(file)
|
||||||
?.let { packageInfo ->
|
?.let { packageInfo ->
|
||||||
SelectedApp.Local(packageName = packageInfo.packageName, version = packageInfo.versionName, file = file)
|
SelectedApp.Local(packageName = packageInfo.packageName, version = packageInfo.versionName, file = file)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import android.content.pm.PackageInfo
|
|||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
|
import app.revanced.manager.domain.installer.RootServiceException
|
||||||
|
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.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.collectEach
|
import app.revanced.manager.util.collectEach
|
||||||
@ -14,7 +17,8 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class InstalledAppsViewModel(
|
class InstalledAppsViewModel(
|
||||||
private val installedAppsRepository: InstalledAppRepository,
|
private val installedAppsRepository: InstalledAppRepository,
|
||||||
private val pm: PM
|
private val pm: PM,
|
||||||
|
private val rootInstaller: RootInstaller
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
|
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
@ -24,8 +28,23 @@ class InstalledAppsViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
apps.collectEach { installedApp ->
|
apps.collectEach { installedApp ->
|
||||||
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
|
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
|
||||||
pm.getPackageInfo(installedApp.currentPackageName)
|
try {
|
||||||
.also { if (it == null) installedAppsRepository.delete(installedApp) }
|
if (
|
||||||
|
installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
|
||||||
|
) {
|
||||||
|
installedAppsRepository.delete(installedApp)
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
} catch (_: RootServiceException) { }
|
||||||
|
|
||||||
|
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
|
||||||
|
|
||||||
|
if (packageInfo == null && installedApp.installType != InstallType.ROOT) {
|
||||||
|
installedAppsRepository.delete(installedApp)
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
packageInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import androidx.work.WorkInfo
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
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.KeystoreManager
|
import app.revanced.manager.domain.manager.KeystoreManager
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
@ -29,7 +31,9 @@ import app.revanced.manager.patcher.worker.Step
|
|||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
import app.revanced.manager.service.UninstallService
|
import app.revanced.manager.service.UninstallService
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.simpleMessage
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@ -48,18 +52,23 @@ import java.util.logging.Level
|
|||||||
import java.util.logging.LogRecord
|
import java.util.logging.LogRecord
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinComponent {
|
class InstallerViewModel(
|
||||||
|
private val input: Destination.Installer
|
||||||
|
) : ViewModel(), KoinComponent {
|
||||||
private val keystoreManager: KeystoreManager by inject()
|
private val keystoreManager: KeystoreManager by inject()
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val installedAppReceiver: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
val packageName: String = input.selectedApp.packageName
|
val packageName: String = input.selectedApp.packageName
|
||||||
private val outputFile = File(app.cacheDir, "output.apk")
|
private val outputFile = File(app.cacheDir, "output.apk")
|
||||||
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
||||||
private var hasSigned = false
|
private var hasSigned = false
|
||||||
|
var inputFile: File? = null
|
||||||
|
|
||||||
|
private var installedApp: InstalledApp? = null
|
||||||
var isInstalling by mutableStateOf(false)
|
var isInstalling by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
var installedPackageName by mutableStateOf<String?>(null)
|
var installedPackageName by mutableStateOf<String?>(null)
|
||||||
@ -73,13 +82,18 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
private val logger = ManagerLogger()
|
private val logger = ManagerLogger()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
installedApp = installedAppRepository.get(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
val (selectedApp, patches, options) = input
|
val (selectedApp, patches, options) = input
|
||||||
|
|
||||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||||
app,
|
app,
|
||||||
patches.flatMap { (_, selected) -> selected },
|
patches.flatMap { (_, selected) -> selected },
|
||||||
input.selectedApp
|
selectedApp
|
||||||
).toImmutableList())
|
).toImmutableList())
|
||||||
|
|
||||||
patcherWorkerId =
|
patcherWorkerId =
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
"patching", PatcherWorker.Args(
|
"patching", PatcherWorker.Args(
|
||||||
@ -90,7 +104,9 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
packageName,
|
packageName,
|
||||||
selectedApp.version,
|
selectedApp.version,
|
||||||
_progress,
|
_progress,
|
||||||
logger
|
logger,
|
||||||
|
selectedApp,
|
||||||
|
setInputFile = { inputFile = it }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -118,7 +134,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
installedPackageName =
|
installedPackageName =
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
installedAppReceiver.add(
|
installedAppRepository.add(
|
||||||
installedPackageName!!,
|
installedPackageName!!,
|
||||||
packageName,
|
packageName,
|
||||||
input.selectedApp.version,
|
input.selectedApp.version,
|
||||||
@ -162,6 +178,19 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
|
|
||||||
outputFile.delete()
|
outputFile.delete()
|
||||||
signedFile.delete()
|
signedFile.delete()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (input.selectedApp is SelectedApp.Installed) {
|
||||||
|
installedApp?.let {
|
||||||
|
if (it.installType == InstallType.ROOT) {
|
||||||
|
rootInstaller.mount(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Failed to mount", e)
|
||||||
|
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun signApk(): Boolean {
|
private suspend fun signApk(): Boolean {
|
||||||
@ -192,20 +221,62 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installOrOpen() = viewModelScope.launch {
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
installedPackageName?.let {
|
|
||||||
pm.launch(it)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
try {
|
try {
|
||||||
if (!signApk()) return@launch
|
if (!signApk()) return@launch
|
||||||
pm.installApp(listOf(signedFile))
|
|
||||||
|
when (installType) {
|
||||||
|
InstallType.DEFAULT -> { pm.installApp(listOf(signedFile)) }
|
||||||
|
|
||||||
|
InstallType.ROOT -> { installAsRoot() }
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
isInstalling = false
|
isInstalling = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun open() = installedPackageName?.let { pm.launch(it) }
|
||||||
|
|
||||||
|
private suspend fun installAsRoot() {
|
||||||
|
try {
|
||||||
|
val label = with(pm) {
|
||||||
|
getPackageInfo(signedFile)?.label()
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
}
|
||||||
|
|
||||||
|
rootInstaller.install(
|
||||||
|
outputFile,
|
||||||
|
inputFile,
|
||||||
|
packageName,
|
||||||
|
input.selectedApp.version,
|
||||||
|
label
|
||||||
|
)
|
||||||
|
|
||||||
|
rootInstaller.mount(packageName)
|
||||||
|
|
||||||
|
installedApp?.let { installedAppRepository.delete(it) }
|
||||||
|
|
||||||
|
installedAppRepository.add(
|
||||||
|
packageName,
|
||||||
|
packageName,
|
||||||
|
input.selectedApp.version,
|
||||||
|
InstallType.ROOT,
|
||||||
|
input.selectedPatches
|
||||||
|
)
|
||||||
|
|
||||||
|
installedPackageName = packageName
|
||||||
|
|
||||||
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
|
} 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) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this to a better place
|
// TODO: move this to a better place
|
||||||
|
@ -7,7 +7,8 @@ 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.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.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
@ -35,8 +36,9 @@ class VersionSelectorViewModel(
|
|||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val appDownloader: AppDownloader = APKMirror()
|
private val appDownloader: AppDownloader = APKMirror()
|
||||||
|
val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
var installedApp: Pair<PackageInfo, Boolean>? by mutableStateOf(null)
|
var installedApp: Pair<PackageInfo, InstalledApp?>? by mutableStateOf(null)
|
||||||
private set
|
private set
|
||||||
var isLoading by mutableStateOf(true)
|
var isLoading by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
@ -72,15 +74,11 @@ class VersionSelectorViewModel(
|
|||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||||
val alreadyPatched = async(Dispatchers.IO) {
|
val installedAppDeferred = async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||||
installedAppRepository.get(packageName)
|
|
||||||
?.let { it.installType == InstallType.DEFAULT }
|
|
||||||
?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
installedApp =
|
installedApp =
|
||||||
packageInfo.await()?.let {
|
packageInfo.await()?.let {
|
||||||
it to alreadyPatched.await()
|
it to installedAppDeferred.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,8 +32,7 @@ private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readab
|
|||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val patches: Int?,
|
val patches: Int?,
|
||||||
val packageInfo: PackageInfo?,
|
val packageInfo: PackageInfo?
|
||||||
val path: File? = null
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
@ -57,8 +56,7 @@ class PM(
|
|||||||
AppInfo(
|
AppInfo(
|
||||||
pkg,
|
pkg,
|
||||||
compatiblePackages[pkg],
|
compatiblePackages[pkg],
|
||||||
packageInfo,
|
packageInfo
|
||||||
File(packageInfo.applicationInfo.sourceDir)
|
|
||||||
)
|
)
|
||||||
} ?: AppInfo(
|
} ?: AppInfo(
|
||||||
pkg,
|
pkg,
|
||||||
@ -73,8 +71,7 @@ class PM(
|
|||||||
AppInfo(
|
AppInfo(
|
||||||
packageInfo.packageName,
|
packageInfo.packageName,
|
||||||
0,
|
0,
|
||||||
packageInfo,
|
packageInfo
|
||||||
File(packageInfo.applicationInfo.sourceDir)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,9 +82,13 @@ class PM(
|
|||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareByDescending<AppInfo> {
|
compareByDescending<AppInfo> {
|
||||||
it.patches
|
it.patches
|
||||||
}.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName }
|
}.thenBy {
|
||||||
|
it.packageInfo?.label()
|
||||||
|
}.thenBy { it.packageName }
|
||||||
)
|
)
|
||||||
} else { emptyList() }
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String): PackageInfo? =
|
fun getPackageInfo(packageName: String): PackageInfo? =
|
||||||
@ -97,6 +98,10 @@ class PM(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPackageInfo(file: File): PackageInfo? = app.packageManager.getPackageArchiveInfo(file.absolutePath, 0)
|
||||||
|
|
||||||
|
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
val packageInstaller = app.packageManager.packageInstaller
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||||
@ -114,7 +119,6 @@ class PM(
|
|||||||
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
app.startActivity(it)
|
app.startActivity(it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||||
apk.inputStream().use { inputStream ->
|
apk.inputStream().use { inputStream ->
|
||||||
@ -153,3 +157,4 @@ private val Context.uninstallIntentSender
|
|||||||
Intent(this, UninstallService::class.java),
|
Intent(this, UninstallService::class.java),
|
||||||
intentFlags
|
intentFlags
|
||||||
).intentSender
|
).intentSender
|
||||||
|
}
|
@ -24,8 +24,6 @@
|
|||||||
<string name="bundle_missing">Missing</string>
|
<string name="bundle_missing">Missing</string>
|
||||||
<string name="bundle_error">Error</string>
|
<string name="bundle_error">Error</string>
|
||||||
|
|
||||||
<string name="select_version">Select version</string>
|
|
||||||
|
|
||||||
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
||||||
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
||||||
<string name="auto_updates_dialog_manager">Manager updates</string>
|
<string name="auto_updates_dialog_manager">Manager updates</string>
|
||||||
@ -156,9 +154,11 @@
|
|||||||
<string name="failed_to_load_apk">Failed to load apk</string>
|
<string name="failed_to_load_apk">Failed to load apk</string>
|
||||||
<string name="loading">Loading…</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="not_installed">Not installed</string>
|
<string name="not_installed">Not installed</string>
|
||||||
|
<string name="installed">Installed</string>
|
||||||
|
|
||||||
<string name="app_info">App info</string>
|
<string name="app_info">App info</string>
|
||||||
<string name="uninstall">Uninstall</string>
|
<string name="uninstall">Uninstall</string>
|
||||||
|
<string name="unpatch">Unpatch</string>
|
||||||
<string name="repatch">Repatch</string>
|
<string name="repatch">Repatch</string>
|
||||||
<string name="install_type">Installation type</string>
|
<string name="install_type">Installation type</string>
|
||||||
<string name="package_name">Package name</string>
|
<string name="package_name">Package name</string>
|
||||||
@ -167,9 +167,20 @@
|
|||||||
<string name="view_applied_patches">View applied patches</string>
|
<string name="view_applied_patches">View applied patches</string>
|
||||||
<string name="default_install">Default</string>
|
<string name="default_install">Default</string>
|
||||||
<string name="root_install">Root</string>
|
<string name="root_install">Root</string>
|
||||||
|
<string name="mounted">Mounted</string>
|
||||||
|
<string name="not_mounted">Not mounted</string>
|
||||||
|
<string name="mount">Mount</string>
|
||||||
|
<string name="unmount">Unmount</string>
|
||||||
|
<string name="failed_to_mount">Failed to mount: %s</string>
|
||||||
|
<string name="failed_to_unmount">Failed to unmount: %s</string>
|
||||||
|
<string name="unpatch_app">Unpatch app?</string>
|
||||||
|
<string name="unpatch_description">Are you sure you want to unpatch this app?</string>
|
||||||
|
|
||||||
<string name="error_occurred">An error occurred</string>
|
<string name="error_occurred">An error occurred</string>
|
||||||
<string name="already_downloaded">Already downloaded</string>
|
<string name="already_downloaded">Already downloaded</string>
|
||||||
|
<string name="select_version">Select version</string>
|
||||||
|
<string name="downloadable_versions">Downloadable versions</string>
|
||||||
|
<string name="already_patched">Already patched</string>
|
||||||
|
|
||||||
<string name="string_option_icon_description">Edit</string>
|
<string name="string_option_icon_description">Edit</string>
|
||||||
<string name="string_option_menu_description">More options</string>
|
<string name="string_option_menu_description">More options</string>
|
||||||
@ -193,6 +204,7 @@
|
|||||||
<string name="export_app_success">Apk exported</string>
|
<string name="export_app_success">Apk exported</string>
|
||||||
<string name="sign_fail">Failed to sign Apk: %s</string>
|
<string name="sign_fail">Failed to sign Apk: %s</string>
|
||||||
<string name="save_logs">Save logs</string>
|
<string name="save_logs">Save logs</string>
|
||||||
|
<string name="select_install_type">Select installation type</string>
|
||||||
|
|
||||||
<string name="patcher_step_group_prepare">Preparation</string>
|
<string name="patcher_step_group_prepare">Preparation</string>
|
||||||
<string name="patcher_step_load_patches">Load patches</string>
|
<string name="patcher_step_load_patches">Load patches</string>
|
||||||
|
@ -26,6 +26,7 @@ aboutLibrariesGradlePlugin = "10.8.2"
|
|||||||
coil = "2.4.0"
|
coil = "2.4.0"
|
||||||
app-icon-loader-coil = "1.5.0"
|
app-icon-loader-coil = "1.5.0"
|
||||||
skrapeit = "1.2.1"
|
skrapeit = "1.2.1"
|
||||||
|
libsu = "5.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
@ -98,6 +99,11 @@ skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.
|
|||||||
# Markdown
|
# Markdown
|
||||||
markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown" }
|
markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown" }
|
||||||
|
|
||||||
|
# LibSU
|
||||||
|
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||||
|
libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
|
||||||
|
libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
|
||||||
|
Loading…
Reference in New Issue
Block a user