mirror of
https://github.com/ReVanced/revanced-library.git
synced 2026-01-21 02:13:56 +00:00
feat: Add local Android installer (#25)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package app.revanced.library.installation.command;
|
||||
|
||||
interface ILocalShellCommandRunnerRootService {
|
||||
IBinder getFileSystemService();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.BuilderImpl
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* The [LocalShellCommandRunner] for running commands locally on the device.
|
||||
*
|
||||
* @param context The [Context] to use for binding to the [RootService].
|
||||
* @param onReady A callback to be invoked when [LocalShellCommandRunner] is ready to be used.
|
||||
* @throws IllegalStateException If the main shell was already created
|
||||
*
|
||||
* @see ShellCommandRunner
|
||||
*/
|
||||
class LocalShellCommandRunner internal constructor(
|
||||
private val context: Context,
|
||||
private val onReady: () -> Unit,
|
||||
) : ShellCommandRunner(), ServiceConnection, Closeable {
|
||||
private var fileSystemManager: FileSystemManager? = null
|
||||
|
||||
init {
|
||||
logger.info("Binding to RootService")
|
||||
val intent = Intent(context, LocalShellCommandRunnerRootService::class.java)
|
||||
RootService.bind(intent, this)
|
||||
}
|
||||
|
||||
override fun runCommand(command: String) = shell.newJob().add(command).exec().let {
|
||||
object : RunResult {
|
||||
override val exitCode = it.code
|
||||
override val output by lazy { it.out.joinToString("\n") }
|
||||
override val error by lazy { it.err.joinToString("\n") }
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasRootPermission() = shell.isRoot
|
||||
|
||||
/**
|
||||
* Writes the given [content] to the given [targetFilePath].
|
||||
*
|
||||
* @param content The [InputStream] to write.
|
||||
* @param targetFilePath The path to write to.
|
||||
* @throws NotReadyException If the [LocalShellCommandRunner] is not ready yet.
|
||||
*/
|
||||
override fun write(content: InputStream, targetFilePath: String) {
|
||||
fileSystemManager?.let {
|
||||
it.getFile(targetFilePath).newOutputStream().use { outputStream ->
|
||||
content.copyTo(outputStream)
|
||||
}
|
||||
} ?: throw NotReadyException("FileSystemManager service is not ready yet")
|
||||
}
|
||||
|
||||
override fun move(file: File, targetFilePath: String) {
|
||||
invoke("mv ${file.absolutePath} $targetFilePath")
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val ipc = ILocalShellCommandRunnerRootService.Stub.asInterface(service)
|
||||
fileSystemManager = FileSystemManager.getRemote(ipc.fileSystemService)
|
||||
|
||||
logger.info("LocalShellCommandRunner service is ready")
|
||||
|
||||
onReady()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
fileSystemManager = null
|
||||
|
||||
logger.info("LocalShellCommandRunner service is disconnected")
|
||||
}
|
||||
|
||||
override fun close() = RootService.unbind(this)
|
||||
|
||||
private companion object {
|
||||
private val shell = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER).build()
|
||||
}
|
||||
|
||||
internal class NotReadyException internal constructor(message: String) : Exception(message)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
|
||||
/**
|
||||
* The [RootService] for the [LocalShellCommandRunner].
|
||||
*/
|
||||
internal class LocalShellCommandRunnerRootService : RootService() {
|
||||
override fun onBind(intent: Intent) = object : ILocalShellCommandRunnerRootService.Stub() {
|
||||
override fun getFileSystemService() =
|
||||
FileSystemManager.getService()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* [LocalInstaller] for installing and uninstalling [Apk] files locally.
|
||||
*
|
||||
* @param context The [Context] to use for installing and uninstalling.
|
||||
* @param onResult The callback to be invoked when the [Apk] is installed or uninstalled.
|
||||
*
|
||||
* @see Installer
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class LocalInstaller(
|
||||
private val context: Context,
|
||||
onResult: (result: LocalInstallerResult) -> Unit,
|
||||
) : Installer<Unit, Installation>(), Closeable {
|
||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val pmStatus = intent.getIntExtra(LocalInstallerService.EXTRA_STATUS, -999)
|
||||
val extra = intent.getStringExtra(LocalInstallerService.EXTRA_STATUS_MESSAGE)!!
|
||||
val packageName = intent.getStringExtra(LocalInstallerService.EXTRA_PACKAGE_NAME)!!
|
||||
|
||||
onResult.invoke(LocalInstallerResult(pmStatus, extra, packageName))
|
||||
}
|
||||
}
|
||||
|
||||
private val intentSender
|
||||
get() = PendingIntent.getService(
|
||||
context,
|
||||
0,
|
||||
Intent(context, LocalInstallerService::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
).intentSender
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
broadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(LocalInstallerService.ACTION)
|
||||
},
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun install(apk: Apk) {
|
||||
logger.info("Installing ${apk.file.name}")
|
||||
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
session.writeApk(apk.file)
|
||||
session.commit(intentSender)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun uninstall(packageName: String) {
|
||||
logger.info("Uninstalling $packageName")
|
||||
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
|
||||
packageInstaller.uninstall(packageName, intentSender)
|
||||
}
|
||||
|
||||
override suspend fun getInstallation(packageName: String) = try {
|
||||
val packageInfo = context.packageManager.getPackageInfo(packageName, 0)
|
||||
|
||||
Installation(packageInfo.applicationInfo.sourceDir)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
override fun close() = context.unregisterReceiver(broadcastReceiver)
|
||||
|
||||
companion object {
|
||||
private val sessionParams = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL,
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, 1024 * 1024)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
|
||||
/**
|
||||
* The result of installing or uninstalling an [Apk] locally using [LocalInstaller].
|
||||
*
|
||||
* @param pmStatus The status code returned by the package manager.
|
||||
* @param extra The extra information returned by the package manager.
|
||||
* @param packageName The package name of the installed app.
|
||||
*
|
||||
* @see LocalInstaller
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class LocalInstallerResult internal constructor(val pmStatus: Int, val extra: String, val packageName: String)
|
||||
@@ -0,0 +1,57 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
class LocalInstallerService : Service() {
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
|
||||
when (extraStatus) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendBroadcast(
|
||||
Intent().apply {
|
||||
action = ACTION
|
||||
`package` = packageName
|
||||
|
||||
putExtra(EXTRA_STATUS, extraStatus)
|
||||
putExtra(EXTRA_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stopSelf()
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
internal companion object {
|
||||
internal const val ACTION = "PACKAGE_INSTALLER_ACTION"
|
||||
|
||||
internal const val EXTRA_STATUS = "EXTRA_STATUS"
|
||||
internal const val EXTRA_STATUS_MESSAGE = "EXTRA_STATUS_MESSAGE"
|
||||
internal const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.installation.command.LocalShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* [LocalRootInstaller] for installing and uninstalling [Apk] files locally with using root permissions by mounting.
|
||||
*
|
||||
* @param context The [Context] to use for binding to the [RootService].
|
||||
* @param onReady A callback to be invoked when [LocalRootInstaller] is ready to be used.
|
||||
*
|
||||
* @throws NoRootPermissionException If the device does not have root permission.
|
||||
*
|
||||
* @see Installer
|
||||
* @see LocalShellCommandRunner
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class LocalRootInstaller(
|
||||
context: Context,
|
||||
onReady: LocalRootInstaller.() -> Unit = {},
|
||||
) : RootInstaller(
|
||||
{ installer ->
|
||||
LocalShellCommandRunner(context) {
|
||||
(installer as LocalRootInstaller).onReady()
|
||||
}
|
||||
},
|
||||
),
|
||||
Closeable {
|
||||
override fun close() = (shellCommandRunner as LocalShellCommandRunner).close()
|
||||
}
|
||||
43
src/commonMain/kotlin/app/revanced/library/Commands.kt
Normal file
43
src/commonMain/kotlin/app/revanced/library/Commands.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
@file:Suppress("DeprecatedCallableAddReplaceWith")
|
||||
|
||||
package app.revanced.library
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.ShellProcessBuilder
|
||||
import java.io.File
|
||||
|
||||
@Deprecated("Do not use this anymore. Instead use AdbCommandRunner.")
|
||||
internal fun JadbDevice.buildCommand(
|
||||
command: String,
|
||||
su: Boolean = true,
|
||||
): ShellProcessBuilder {
|
||||
if (su) return shellProcessBuilder("su -c \'$command\'")
|
||||
|
||||
val args = command.split(" ") as ArrayList<String>
|
||||
val cmd = args.removeFirst()
|
||||
|
||||
return shellProcessBuilder(cmd, *args.toTypedArray())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.run(
|
||||
command: String,
|
||||
su: Boolean = true,
|
||||
) = buildCommand(command, su).start()
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.hasSu() = AdbShellCommandRunner(this).hasRootPermission()
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.push(
|
||||
file: File,
|
||||
targetFilePath: String,
|
||||
) = AdbShellCommandRunner(this).move(file, targetFilePath)
|
||||
|
||||
@Deprecated("Use AdbShellCommandRunner instead.")
|
||||
internal fun JadbDevice.createFile(
|
||||
targetFile: String,
|
||||
content: String,
|
||||
) = AdbShellCommandRunner(this).write(content.byteInputStream(), targetFile)
|
||||
18
src/commonMain/kotlin/app/revanced/library/Utils.kt
Normal file
18
src/commonMain/kotlin/app/revanced/library/Utils.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package app.revanced.library
|
||||
|
||||
/**
|
||||
* Utils for the library.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object Utils {
|
||||
/**
|
||||
* True if the environment is Android.
|
||||
*/
|
||||
val isAndroidEnvironment =
|
||||
try {
|
||||
Class.forName("android.app.Application")
|
||||
true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
144
src/commonMain/kotlin/app/revanced/library/adb/AdbManager.kt
Normal file
144
src/commonMain/kotlin/app/revanced/library/adb/AdbManager.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package app.revanced.library.adb
|
||||
|
||||
import app.revanced.library.adb.AdbManager.Apk
|
||||
import app.revanced.library.installation.installer.AdbInstaller
|
||||
import app.revanced.library.installation.installer.AdbRootInstaller
|
||||
import app.revanced.library.installation.installer.Constants.PLACEHOLDER
|
||||
import app.revanced.library.installation.installer.Installer
|
||||
import app.revanced.library.run
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* [AdbManager] to install and uninstall [Apk] files.
|
||||
*
|
||||
* @param deviceSerial The serial of the device. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use an implementation of Installer instead.")
|
||||
@Suppress("unused")
|
||||
sealed class AdbManager private constructor(
|
||||
@Suppress("UNUSED_PARAMETER") deviceSerial: String?,
|
||||
) {
|
||||
protected abstract val installer: Installer<*, *>
|
||||
|
||||
/**
|
||||
* Installs the [Apk] file.
|
||||
*
|
||||
* @param apk The [Apk] file.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use Installer.install instead.")
|
||||
open fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the package.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use Installer.uninstall instead.")
|
||||
open fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
|
||||
@Deprecated("Use Installer instead.")
|
||||
companion object {
|
||||
/**
|
||||
* Gets an [AdbManager] for the supplied device serial.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
* @param root Whether to use root or not.
|
||||
* @return The [AdbManager].
|
||||
* @throws DeviceNotFoundException If the device can not be found.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("This is deprecated.")
|
||||
fun getAdbManager(
|
||||
deviceSerial: String? = null,
|
||||
root: Boolean = false,
|
||||
): AdbManager = if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use AdbRootInstaller instead.", ReplaceWith("AdbRootInstaller(deviceSerial)"))
|
||||
class RootAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
override val installer = AdbRootInstaller(deviceSerial)
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbRootInstaller.install instead.")
|
||||
override fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbRootInstaller.uninstall instead.")
|
||||
override fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
|
||||
@Deprecated("This is deprecated.")
|
||||
companion object Utils {
|
||||
private fun JadbDevice.run(
|
||||
command: String,
|
||||
with: String,
|
||||
) = run(command.applyReplacement(with))
|
||||
|
||||
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for non-rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
@Deprecated("Use AdbInstaller instead.")
|
||||
class UserAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
override val installer = AdbInstaller(deviceSerial)
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbInstaller.install instead.")
|
||||
override fun install(apk: Apk) = suspend {
|
||||
installer.install(Installer.Apk(apk.file, apk.packageName))
|
||||
}
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use AdbInstaller.uninstall instead.")
|
||||
override fun uninstall(packageName: String) = suspend {
|
||||
installer.uninstall(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apk file for [AdbManager].
|
||||
*
|
||||
* @param file The [Apk] file.
|
||||
* @param packageName The package name of the [Apk] file.
|
||||
*/
|
||||
@Deprecated("Use Installer.Apk instead.")
|
||||
class Apk(val file: File, val packageName: String? = null)
|
||||
|
||||
@Deprecated("Use AdbCommandRunner.DeviceNotFoundException instead.")
|
||||
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) :
|
||||
Exception(
|
||||
deviceSerial?.let {
|
||||
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||
} ?: "No ADB device found",
|
||||
)
|
||||
|
||||
@Deprecated("Use RootInstaller.FailedToFindInstalledPackageException instead.")
|
||||
class FailedToFindInstalledPackageException internal constructor(packageName: String) :
|
||||
Exception("Failed to find installed package \"$packageName\" because no activity was found")
|
||||
|
||||
@Deprecated("Use RootInstaller.PackageNameRequiredException instead.")
|
||||
class PackageNameRequiredException internal constructor() :
|
||||
Exception("Package name is required")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import app.revanced.library.installation.installer.Utils
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.RemoteFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* [AdbShellCommandRunner] for running commands on a device remotely using ADB.
|
||||
*
|
||||
* @see ShellCommandRunner
|
||||
*/
|
||||
class AdbShellCommandRunner : ShellCommandRunner {
|
||||
private val device: JadbDevice
|
||||
|
||||
/**
|
||||
* Creates a [AdbShellCommandRunner] for the given device.
|
||||
*
|
||||
* @param device The device.
|
||||
*/
|
||||
internal constructor(device: JadbDevice) {
|
||||
this.device = device
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [AdbShellCommandRunner] for the device with the given serial.
|
||||
*
|
||||
* @param deviceSerial deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
internal constructor(deviceSerial: String?) {
|
||||
device = Utils.getDevice(deviceSerial, logger)
|
||||
}
|
||||
|
||||
override fun runCommand(command: String) = device.shellProcessBuilder(command).start().let { process ->
|
||||
object : RunResult {
|
||||
override val exitCode by lazy { process.waitFor() }
|
||||
override val output by lazy { process.inputStream.bufferedReader().readText() }
|
||||
override val error by lazy { process.errorStream.bufferedReader().readText() }
|
||||
|
||||
override fun waitFor() {
|
||||
process.waitFor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasRootPermission(): Boolean = invoke("whoami").exitCode == 0
|
||||
|
||||
override fun write(content: InputStream, targetFilePath: String) =
|
||||
device.push(content, System.currentTimeMillis(), 644, RemoteFile(targetFilePath))
|
||||
|
||||
/**
|
||||
* Moves the given [file] from the local to the target file path on the device.
|
||||
*
|
||||
* @param file The file to move.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
override fun move(file: File, targetFilePath: String) = device.push(file, RemoteFile(targetFilePath))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
/**
|
||||
* The result of a command execution.
|
||||
*/
|
||||
interface RunResult {
|
||||
/**
|
||||
* The exit code of the command.
|
||||
*/
|
||||
val exitCode: Int
|
||||
|
||||
/**
|
||||
* The output of the command.
|
||||
*/
|
||||
val output: String
|
||||
|
||||
/**
|
||||
* The error of the command.
|
||||
*/
|
||||
val error: String
|
||||
|
||||
/**
|
||||
* Waits for the command to finish.
|
||||
*/
|
||||
fun waitFor() {}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package app.revanced.library.installation.command
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* [ShellCommandRunner] for running commands on a device.
|
||||
*/
|
||||
abstract class ShellCommandRunner internal constructor() {
|
||||
protected val logger: Logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
/**
|
||||
* Writes the given [content] to the file at the given [targetFilePath] path.
|
||||
*
|
||||
* @param content The content of the file.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
internal abstract fun write(
|
||||
content: InputStream,
|
||||
targetFilePath: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Moves the given [file] to the given [targetFilePath] path.
|
||||
*
|
||||
* @param file The file to move.
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
internal abstract fun move(
|
||||
file: File,
|
||||
targetFilePath: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Runs the given [command] on the device as root.
|
||||
*
|
||||
* @param command The command to run.
|
||||
* @return The [RunResult].
|
||||
*/
|
||||
protected abstract fun runCommand(command: String): RunResult
|
||||
|
||||
/**
|
||||
* Checks if the device has root permission.
|
||||
*
|
||||
* @return True if the device has root permission, false otherwise.
|
||||
*/
|
||||
internal abstract fun hasRootPermission(): Boolean
|
||||
|
||||
/**
|
||||
* Runs a command on the device as root.
|
||||
*
|
||||
* @param command The command to run.
|
||||
* @return The [RunResult].
|
||||
*/
|
||||
internal operator fun invoke(
|
||||
command: String,
|
||||
) = runCommand("su -c \'$command\'")
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import se.vidstige.jadb.JadbException
|
||||
import se.vidstige.jadb.managers.Package
|
||||
import se.vidstige.jadb.managers.PackageManager
|
||||
|
||||
/**
|
||||
* [AdbInstaller] for installing and uninstalling [Apk] files using ADB.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*
|
||||
* @see Installer
|
||||
*/
|
||||
class AdbInstaller(
|
||||
deviceSerial: String? = null,
|
||||
) : Installer<AdbInstallerResult, Installation>() {
|
||||
private val device = Utils.getDevice(deviceSerial, logger)
|
||||
private val adbShellCommandRunner = AdbShellCommandRunner(device)
|
||||
private val packageManager = PackageManager(device)
|
||||
|
||||
init {
|
||||
logger.fine("Connected to $deviceSerial")
|
||||
}
|
||||
|
||||
override suspend fun install(apk: Apk): AdbInstallerResult {
|
||||
logger.info("Installing ${apk.file.name}")
|
||||
|
||||
return runPackageManager { install(apk.file) }
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: String): AdbInstallerResult {
|
||||
logger.info("Uninstalling $packageName")
|
||||
|
||||
return runPackageManager { uninstall(Package(packageName)) }
|
||||
}
|
||||
|
||||
override suspend fun getInstallation(packageName: String): Installation? = packageManager.packages.find {
|
||||
it.toString() == packageName
|
||||
}?.let { Installation(adbShellCommandRunner(INSTALLED_APK_PATH).output) }
|
||||
|
||||
private fun runPackageManager(block: PackageManager.() -> Unit) = try {
|
||||
packageManager.run(block)
|
||||
|
||||
AdbInstallerResult.Success
|
||||
} catch (e: JadbException) {
|
||||
AdbInstallerResult.Failure(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
|
||||
/**
|
||||
* The result of installing or uninstalling an [Apk] via ADB using [AdbInstaller].
|
||||
*
|
||||
* @see AdbInstaller
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
interface AdbInstallerResult {
|
||||
/**
|
||||
* The result of installing an [Apk] successfully.
|
||||
*/
|
||||
object Success : AdbInstallerResult
|
||||
|
||||
/**
|
||||
* The result of installing an [Apk] unsuccessfully.
|
||||
*
|
||||
* @param exception The exception that caused the installation to fail.
|
||||
*/
|
||||
class Failure internal constructor(val exception: Exception) : AdbInstallerResult
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.AdbShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException
|
||||
|
||||
/**
|
||||
* [AdbRootInstaller] for installing and uninstalling [Apk] files with using ADB root permissions by mounting.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*
|
||||
* @throws NoRootPermissionException If the device does not have root permission.
|
||||
*
|
||||
* @see RootInstaller
|
||||
* @see AdbShellCommandRunner
|
||||
*/
|
||||
class AdbRootInstaller(
|
||||
deviceSerial: String? = null,
|
||||
) : RootInstaller({ AdbShellCommandRunner(deviceSerial) }) {
|
||||
init {
|
||||
logger.fine("Connected to $deviceSerial")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
internal object Constants {
|
||||
const val PLACEHOLDER = "PLACEHOLDER"
|
||||
|
||||
const val TMP_FILE_PATH = "/data/local/tmp/revanced.tmp"
|
||||
const val MOUNT_PATH = "/data/adb/revanced/"
|
||||
const val MOUNTED_APK_PATH = "$MOUNT_PATH$PLACEHOLDER.apk"
|
||||
const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
|
||||
|
||||
const val EXISTS = "[[ -f $PLACEHOLDER ]] || exit 1"
|
||||
const val MOUNT_GREP = "grep $PLACEHOLDER /proc/mounts"
|
||||
const val DELETE = "rm -rf $PLACEHOLDER"
|
||||
const val CREATE_DIR = "mkdir -p"
|
||||
const val RESTART = "am start -S $PLACEHOLDER"
|
||||
const val KILL = "am force-stop $PLACEHOLDER"
|
||||
const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER"
|
||||
const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH"
|
||||
|
||||
const val MOUNT_APK =
|
||||
"base_path=\"$MOUNTED_APK_PATH\" && " +
|
||||
"mv $TMP_FILE_PATH \$base_path && " +
|
||||
"chmod 644 \$base_path && " +
|
||||
"chown system:system \$base_path && " +
|
||||
"chcon u:object_r:apk_data_file:s0 \$base_path"
|
||||
|
||||
const val UMOUNT =
|
||||
"grep $PLACEHOLDER /proc/mounts | " +
|
||||
"while read -r line; do echo \$line | " +
|
||||
"cut -d ' ' -f 2 | " +
|
||||
"sed 's/apk.*/apk/' | " +
|
||||
"xargs -r umount -l; done"
|
||||
|
||||
const val INSTALL_MOUNT_SCRIPT = "mv $TMP_FILE_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH"
|
||||
|
||||
val MOUNT_SCRIPT =
|
||||
"""
|
||||
#!/system/bin/sh
|
||||
until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
|
||||
stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
|
||||
# Make sure the app is installed.
|
||||
if [ -z "${'$'}stock_path" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unmount any existing installations to prevent multiple unnecessary mounts.
|
||||
$UMOUNT
|
||||
|
||||
base_path="$MOUNTED_APK_PATH"
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||
|
||||
# Use Magisk mirror, if possible.
|
||||
if command -v magisk &> /dev/null; then
|
||||
MIRROR="${'$'}(magisk --path)/.magisk/mirror"
|
||||
fi
|
||||
|
||||
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
|
||||
|
||||
# Kill the app to force it to restart the mounted APK in case it's currently running.
|
||||
$KILL
|
||||
""".trimIndent()
|
||||
|
||||
/**
|
||||
* Replaces the [PLACEHOLDER] with the given [replacement].
|
||||
*
|
||||
* @param replacement The replacement to use.
|
||||
* @return The replaced string.
|
||||
*/
|
||||
operator fun String.invoke(replacement: String) = replace(PLACEHOLDER, replacement)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
/**
|
||||
* [Installation] of an apk file.
|
||||
*
|
||||
* @param apkFilePath The apk file path.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class Installation internal constructor(
|
||||
val apkFilePath: String,
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* [Installer] for installing and uninstalling [Apk] files.
|
||||
*
|
||||
* @param TInstallerResult The type of the result of the installation.
|
||||
* @param TInstallation The type of the installation.
|
||||
*/
|
||||
abstract class Installer<TInstallerResult, TInstallation : Installation> internal constructor() {
|
||||
/**
|
||||
* The [Logger].
|
||||
*/
|
||||
protected val logger: Logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
/**
|
||||
* Installs the [Apk] file.
|
||||
*
|
||||
* @param apk The [Apk] file.
|
||||
*
|
||||
* @return The result of the installation.
|
||||
*/
|
||||
abstract suspend fun install(apk: Apk): TInstallerResult
|
||||
|
||||
/**
|
||||
* Uninstalls the package.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*
|
||||
* @return The result of the uninstallation.
|
||||
*/
|
||||
abstract suspend fun uninstall(packageName: String): TInstallerResult
|
||||
|
||||
/**
|
||||
* Gets the current installation or null if not installed.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*
|
||||
* @return The installation.
|
||||
*/
|
||||
abstract suspend fun getInstallation(packageName: String): TInstallation?
|
||||
|
||||
/**
|
||||
* Apk file for [Installer].
|
||||
*
|
||||
* @param file The [Apk] file.
|
||||
* @param packageName The package name of the [Apk] file.
|
||||
*/
|
||||
class Apk(val file: File, val packageName: String? = null)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
/**
|
||||
* [RootInstallation] of the apk file that is mounted to [installedApkFilePath] with root permissions.
|
||||
*
|
||||
* @param installedApkFilePath The installed apk file path or null if the apk is not installed.
|
||||
* @param apkFilePath The mounting apk file path.
|
||||
* @param mounted Whether the apk is mounted to [installedApkFilePath].
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class RootInstallation internal constructor(
|
||||
val installedApkFilePath: String?,
|
||||
apkFilePath: String,
|
||||
val mounted: Boolean,
|
||||
) : Installation(apkFilePath)
|
||||
@@ -0,0 +1,135 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.command.ShellCommandRunner
|
||||
import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION_PATH
|
||||
import app.revanced.library.installation.installer.Constants.DELETE
|
||||
import app.revanced.library.installation.installer.Constants.EXISTS
|
||||
import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT
|
||||
import app.revanced.library.installation.installer.Constants.KILL
|
||||
import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_APK
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_GREP
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT
|
||||
import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH
|
||||
import app.revanced.library.installation.installer.Constants.RESTART
|
||||
import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH
|
||||
import app.revanced.library.installation.installer.Constants.UMOUNT
|
||||
import app.revanced.library.installation.installer.Constants.invoke
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* [RootInstaller] for installing and uninstalling [Apk] files using root permissions by mounting.
|
||||
*
|
||||
* @param shellCommandRunnerSupplier A supplier for the [ShellCommandRunner] to use.
|
||||
*
|
||||
* @throws NoRootPermissionException If the device does not have root permission.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class RootInstaller internal constructor(
|
||||
shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner,
|
||||
) : Installer<RootInstallerResult, RootInstallation>() {
|
||||
|
||||
/**
|
||||
* The command runner used to run commands on the device.
|
||||
*/
|
||||
@Suppress("LeakingThis")
|
||||
protected val shellCommandRunner = shellCommandRunnerSupplier(this)
|
||||
|
||||
init {
|
||||
if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException()
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the given [apk] by mounting.
|
||||
*
|
||||
* @param apk The [Apk] to install.
|
||||
*
|
||||
* @throws PackageNameRequiredException If the [Apk] does not have a package name.
|
||||
*/
|
||||
override suspend fun install(apk: Apk): RootInstallerResult {
|
||||
logger.info("Installing ${apk.packageName} by mounting")
|
||||
|
||||
val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException()
|
||||
|
||||
// Setup files.
|
||||
apk.file.move(TMP_FILE_PATH)
|
||||
CREATE_INSTALLATION_PATH().waitFor()
|
||||
MOUNT_APK(packageName)().waitFor()
|
||||
|
||||
// Install and run.
|
||||
TMP_FILE_PATH.write(MOUNT_SCRIPT(packageName))
|
||||
INSTALL_MOUNT_SCRIPT(packageName)().waitFor()
|
||||
MOUNT_SCRIPT_PATH(packageName)().waitFor()
|
||||
RESTART(packageName)()
|
||||
|
||||
DELETE(TMP_FILE_PATH)()
|
||||
|
||||
return RootInstallerResult.SUCCESS
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: String): RootInstallerResult {
|
||||
logger.info("Uninstalling $packageName by unmounting")
|
||||
|
||||
UMOUNT(packageName)()
|
||||
|
||||
DELETE(MOUNTED_APK_PATH)(packageName)()
|
||||
DELETE(MOUNT_SCRIPT_PATH)(packageName)()
|
||||
DELETE(TMP_FILE_PATH)() // Remove residual.
|
||||
|
||||
KILL(packageName)()
|
||||
|
||||
return RootInstallerResult.SUCCESS
|
||||
}
|
||||
|
||||
override suspend fun getInstallation(packageName: String): RootInstallation? {
|
||||
val patchedApkPath = MOUNTED_APK_PATH(packageName)
|
||||
|
||||
val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0
|
||||
if (patchedApkExists) return null
|
||||
|
||||
return RootInstallation(
|
||||
INSTALLED_APK_PATH(packageName)().output.ifEmpty { null },
|
||||
patchedApkPath,
|
||||
MOUNT_GREP(patchedApkPath)().exitCode == 0,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command on the device.
|
||||
*/
|
||||
protected operator fun String.invoke() = shellCommandRunner(this)
|
||||
|
||||
/**
|
||||
* Moves the given file to the given [targetFilePath].
|
||||
*
|
||||
* @param targetFilePath The target file path.
|
||||
*/
|
||||
protected fun File.move(targetFilePath: String) = shellCommandRunner.move(this, targetFilePath)
|
||||
|
||||
/**
|
||||
* Writes the given [content] to the file.
|
||||
*
|
||||
* @param content The content of the file.
|
||||
*/
|
||||
protected fun String.write(content: String) = shellCommandRunner.write(content.byteInputStream(), this)
|
||||
|
||||
/**
|
||||
* Asserts that the package is installed.
|
||||
*
|
||||
* @throws FailedToFindInstalledPackageException If the package is not installed.
|
||||
*/
|
||||
private fun String.assertInstalled() {
|
||||
if (INSTALLED_APK_PATH(this)().output.isNotEmpty()) {
|
||||
throw FailedToFindInstalledPackageException(this)
|
||||
}
|
||||
}
|
||||
|
||||
internal class FailedToFindInstalledPackageException internal constructor(packageName: String) :
|
||||
Exception("Failed to find installed package \"$packageName\" because no activity was found")
|
||||
|
||||
internal class PackageNameRequiredException internal constructor() : Exception("Package name is required")
|
||||
internal class NoRootPermissionException internal constructor() : Exception("No root permission")
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import app.revanced.library.installation.installer.Installer.Apk
|
||||
|
||||
/**
|
||||
* The result of installing or uninstalling an [Apk] with root permissions using [RootInstaller].
|
||||
*
|
||||
* @see RootInstaller
|
||||
*/
|
||||
enum class RootInstallerResult {
|
||||
/**
|
||||
* The result of installing an [Apk] successfully.
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* The result of installing an [Apk] unsuccessfully.
|
||||
*/
|
||||
FAILURE,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package app.revanced.library.installation.installer
|
||||
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Utility functions for [Installer].
|
||||
*
|
||||
* @see Installer
|
||||
*/
|
||||
internal object Utils {
|
||||
/**
|
||||
* Gets the device with the given serial.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
* @param logger The logger.
|
||||
* @return The device.
|
||||
* @throws DeviceNotFoundException If no device with the given serial is found.
|
||||
*/
|
||||
internal fun getDevice(
|
||||
deviceSerial: String? = null,
|
||||
logger: Logger,
|
||||
) = with(JadbConnection().devices) {
|
||||
if (isEmpty()) throw DeviceNotFoundException()
|
||||
|
||||
deviceSerial?.let {
|
||||
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(
|
||||
deviceSerial,
|
||||
)
|
||||
} ?: first().also {
|
||||
logger.warning("No device serial supplied. Using device with serial ${it.serial}")
|
||||
}
|
||||
}!!
|
||||
|
||||
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) : Exception(
|
||||
deviceSerial?.let {
|
||||
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||
} ?: "No ADB device found",
|
||||
)
|
||||
}
|
||||
@@ -5,15 +5,17 @@ import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.SimpleFormatter
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object Logger {
|
||||
/**
|
||||
* Rules for allowed loggers.
|
||||
*/
|
||||
private val allowedLoggersRules =
|
||||
arrayOf<String.() -> Boolean>(
|
||||
{ startsWith("app.revanced") }, // ReVanced loggers.
|
||||
{ this == "" }, // Logs warnings when compiling resources (Logger in class brut.util.OS).
|
||||
// ReVanced loggers.
|
||||
{ startsWith("app.revanced") },
|
||||
// Logs warnings when compiling resources (Logger in class brut.util.OS).
|
||||
{ this == "" },
|
||||
)
|
||||
|
||||
private val rootLogger = java.util.logging.Logger.getLogger("")
|
||||
@@ -6,36 +6,29 @@ import app.revanced.patcher.patch.BytecodePatch
|
||||
import app.revanced.patcher.patch.annotation.Patch
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import org.junit.jupiter.api.MethodOrderer
|
||||
import org.junit.jupiter.api.Order
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestMethodOrder
|
||||
import kotlin.test.Test
|
||||
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
||||
internal object PatchOptionsTest {
|
||||
class PatchOptionsTest {
|
||||
private var patches = setOf(PatchOptionsTestPatch)
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
fun serializeTest() {
|
||||
assert(SERIALIZED_JSON == Options.serialize(patches))
|
||||
}
|
||||
private val serializedJson =
|
||||
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":null},{\"key\":\"key2\"," +
|
||||
"\"value\":true}]}]"
|
||||
|
||||
private val changedJson =
|
||||
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2" +
|
||||
"\",\"value\":false}]}]"
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
fun loadOptionsTest() {
|
||||
patches.setOptions(CHANGED_JSON)
|
||||
fun `serializes and deserializes`() {
|
||||
assert(serializedJson == Options.serialize(patches))
|
||||
|
||||
patches.setOptions(changedJson)
|
||||
|
||||
assert(PatchOptionsTestPatch.option1 == "test")
|
||||
assert(PatchOptionsTestPatch.option2 == false)
|
||||
}
|
||||
|
||||
private const val SERIALIZED_JSON =
|
||||
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":null},{\"key\":\"key2\",\"value\":true}]}]"
|
||||
|
||||
private const val CHANGED_JSON =
|
||||
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]"
|
||||
|
||||
@Patch("PatchOptionsTestPatch")
|
||||
object PatchOptionsTestPatch : BytecodePatch(emptySet()) {
|
||||
var option1 by stringPatchOption("key1", null, null, "title1", "description1")
|
||||
@@ -7,12 +7,12 @@ import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intArrayPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal object PatchUtilsTest {
|
||||
internal class PatchUtilsTest {
|
||||
private val patches =
|
||||
arrayOf(
|
||||
newPatch("some.package", setOf("a")) { stringPatchOption("string", "value") },
|
||||
@@ -1,183 +0,0 @@
|
||||
package app.revanced.library.adb
|
||||
|
||||
import app.revanced.library.adb.AdbManager.Apk
|
||||
import app.revanced.library.adb.Constants.CREATE_DIR
|
||||
import app.revanced.library.adb.Constants.DELETE
|
||||
import app.revanced.library.adb.Constants.GET_INSTALLED_PATH
|
||||
import app.revanced.library.adb.Constants.INSTALLATION_PATH
|
||||
import app.revanced.library.adb.Constants.INSTALL_MOUNT_SCRIPT
|
||||
import app.revanced.library.adb.Constants.INSTALL_PATCHED_APK
|
||||
import app.revanced.library.adb.Constants.KILL
|
||||
import app.revanced.library.adb.Constants.MOUNT_SCRIPT
|
||||
import app.revanced.library.adb.Constants.MOUNT_SCRIPT_PATH
|
||||
import app.revanced.library.adb.Constants.PATCHED_APK_PATH
|
||||
import app.revanced.library.adb.Constants.PLACEHOLDER
|
||||
import app.revanced.library.adb.Constants.RESTART
|
||||
import app.revanced.library.adb.Constants.TMP_PATH
|
||||
import app.revanced.library.adb.Constants.UMOUNT
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.managers.Package
|
||||
import se.vidstige.jadb.managers.PackageManager
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Adb manager. Used to install and uninstall [Apk] files.
|
||||
*
|
||||
* @param deviceSerial The serial of the device. If null, the first connected device will be used.
|
||||
*/
|
||||
sealed class AdbManager private constructor(deviceSerial: String?) {
|
||||
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
|
||||
|
||||
protected val device =
|
||||
with(JadbConnection().devices) {
|
||||
if (isEmpty()) throw DeviceNotFoundException()
|
||||
|
||||
deviceSerial?.let {
|
||||
firstOrNull { it.serial == deviceSerial } ?: throw DeviceNotFoundException(deviceSerial)
|
||||
} ?: first().also {
|
||||
logger.warning("No device serial supplied. Using device with serial ${it.serial}")
|
||||
}
|
||||
}!!
|
||||
|
||||
init {
|
||||
logger.fine("Connected to ${device.serial}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the [Apk] file.
|
||||
*
|
||||
* @param apk The [Apk] file.
|
||||
*/
|
||||
open fun install(apk: Apk) {
|
||||
logger.info("Finished installing ${apk.file.name}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the package.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*/
|
||||
open fun uninstall(packageName: String) {
|
||||
logger.info("Finished uninstalling $packageName")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Gets an [AdbManager] for the supplied device serial.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
* @param root Whether to use root or not.
|
||||
* @return The [AdbManager].
|
||||
* @throws DeviceNotFoundException If the device can not be found.
|
||||
*/
|
||||
fun getAdbManager(
|
||||
deviceSerial: String? = null,
|
||||
root: Boolean = false,
|
||||
): AdbManager = if (root) RootAdbManager(deviceSerial) else UserAdbManager(deviceSerial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
class RootAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
init {
|
||||
if (!device.hasSu()) throw IllegalArgumentException("Root required on ${device.serial}. Task failed")
|
||||
}
|
||||
|
||||
override fun install(apk: Apk) {
|
||||
logger.info("Installing by mounting")
|
||||
|
||||
val packageName = apk.packageName ?: throw PackageNameRequiredException()
|
||||
|
||||
device.run(GET_INSTALLED_PATH, packageName).inputStream.bufferedReader().readLine().let { line ->
|
||||
if (line != null) return@let
|
||||
throw throw FailedToFindInstalledPackageException(packageName)
|
||||
}
|
||||
|
||||
device.push(apk.file, TMP_PATH)
|
||||
|
||||
device.run("$CREATE_DIR $INSTALLATION_PATH").waitFor()
|
||||
device.run(INSTALL_PATCHED_APK, packageName).waitFor()
|
||||
|
||||
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement(packageName))
|
||||
|
||||
device.run(INSTALL_MOUNT_SCRIPT, packageName).waitFor()
|
||||
device.run(MOUNT_SCRIPT_PATH, packageName).waitFor()
|
||||
device.run(RESTART, packageName)
|
||||
device.run(DELETE, TMP_PATH)
|
||||
|
||||
super.install(apk)
|
||||
}
|
||||
|
||||
override fun uninstall(packageName: String) {
|
||||
logger.info("Uninstalling $packageName by unmounting")
|
||||
|
||||
device.run(UMOUNT, packageName)
|
||||
device.run(DELETE.applyReplacement(PATCHED_APK_PATH), packageName)
|
||||
device.run(DELETE, MOUNT_SCRIPT_PATH.applyReplacement(packageName))
|
||||
device.run(DELETE, TMP_PATH)
|
||||
device.run(KILL, packageName)
|
||||
|
||||
super.uninstall(packageName)
|
||||
}
|
||||
|
||||
companion object Utils {
|
||||
private fun JadbDevice.run(
|
||||
command: String,
|
||||
with: String,
|
||||
) = run(command.applyReplacement(with))
|
||||
|
||||
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adb manager for non-rooted devices.
|
||||
*
|
||||
* @param deviceSerial The device serial. If null, the first connected device will be used.
|
||||
*/
|
||||
class UserAdbManager internal constructor(deviceSerial: String?) : AdbManager(deviceSerial) {
|
||||
private val packageManager = PackageManager(device)
|
||||
|
||||
override fun install(apk: Apk) {
|
||||
logger.info("Installing ${apk.file.name}")
|
||||
|
||||
PackageManager(device).install(apk.file)
|
||||
|
||||
super.install(apk)
|
||||
}
|
||||
|
||||
override fun uninstall(packageName: String) {
|
||||
logger.info("Uninstalling $packageName")
|
||||
|
||||
packageManager.uninstall(Package(packageName))
|
||||
|
||||
super.uninstall(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apk file for [AdbManager].
|
||||
*
|
||||
* @param file The [Apk] file.
|
||||
* @param packageName The package name of the [Apk] file.
|
||||
*/
|
||||
class Apk(val file: File, val packageName: String? = null)
|
||||
|
||||
class DeviceNotFoundException internal constructor(deviceSerial: String? = null) :
|
||||
Exception(
|
||||
deviceSerial?.let {
|
||||
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||
} ?: "No ADB device found",
|
||||
)
|
||||
|
||||
class FailedToFindInstalledPackageException internal constructor(packageName: String) :
|
||||
Exception("Failed to find installed package \"$packageName\" because no activity was found")
|
||||
|
||||
class PackageNameRequiredException internal constructor() :
|
||||
Exception("Package name is required")
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package app.revanced.library.adb
|
||||
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.RemoteFile
|
||||
import se.vidstige.jadb.ShellProcessBuilder
|
||||
import java.io.File
|
||||
|
||||
internal fun JadbDevice.buildCommand(
|
||||
command: String,
|
||||
su: Boolean = true,
|
||||
): ShellProcessBuilder {
|
||||
if (su) return shellProcessBuilder("su -c \'$command\'")
|
||||
|
||||
val args = command.split(" ") as ArrayList<String>
|
||||
val cmd = args.removeFirst()
|
||||
|
||||
return shellProcessBuilder(cmd, *args.toTypedArray())
|
||||
}
|
||||
|
||||
internal fun JadbDevice.run(
|
||||
command: String,
|
||||
su: Boolean = true,
|
||||
) = this.buildCommand(command, su).start()
|
||||
|
||||
internal fun JadbDevice.hasSu() = this.run("whoami", true).waitFor() == 0
|
||||
|
||||
internal fun JadbDevice.push(
|
||||
file: File,
|
||||
targetFilePath: String,
|
||||
) = push(file, RemoteFile(targetFilePath))
|
||||
|
||||
internal fun JadbDevice.createFile(
|
||||
targetFile: String,
|
||||
content: String,
|
||||
) = push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
|
||||
@@ -1,54 +0,0 @@
|
||||
package app.revanced.library.adb
|
||||
|
||||
internal object Constants {
|
||||
internal const val PLACEHOLDER = "PLACEHOLDER"
|
||||
|
||||
internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
|
||||
internal const val INSTALLATION_PATH = "/data/adb/revanced/"
|
||||
internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
|
||||
internal const val MOUNT_SCRIPT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
|
||||
|
||||
internal const val DELETE = "rm -rf $PLACEHOLDER"
|
||||
internal const val CREATE_DIR = "mkdir -p"
|
||||
internal const val RESTART = "am start -S $PLACEHOLDER"
|
||||
internal const val KILL = "am force-stop $PLACEHOLDER"
|
||||
internal const val GET_INSTALLED_PATH = "pm path $PLACEHOLDER"
|
||||
|
||||
internal const val INSTALL_PATCHED_APK =
|
||||
"base_path=\"$PATCHED_APK_PATH\" && " +
|
||||
"mv $TMP_PATH ${'$'}base_path && " +
|
||||
"chmod 644 ${'$'}base_path && " +
|
||||
"chown system:system ${'$'}base_path && " +
|
||||
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
|
||||
|
||||
internal const val UMOUNT =
|
||||
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
|
||||
|
||||
internal const val INSTALL_MOUNT_SCRIPT = "mv $TMP_PATH $MOUNT_SCRIPT_PATH && chmod +x $MOUNT_SCRIPT_PATH"
|
||||
|
||||
internal val MOUNT_SCRIPT =
|
||||
"""
|
||||
#!/system/bin/sh
|
||||
|
||||
# Use Magisk mirror, if possible.
|
||||
if command -v magisk &> /dev/null; then
|
||||
MIRROR="${'$'}(magisk --path)/.magisk/mirror"
|
||||
fi
|
||||
|
||||
# Wait for the system to boot.
|
||||
until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done
|
||||
until [ -d "/sdcard/Android" ]; do sleep 1; done
|
||||
|
||||
# Unmount any existing mount as a safety measure.
|
||||
$UMOUNT
|
||||
|
||||
base_path="$PATCHED_APK_PATH"
|
||||
stock_path=$( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||
|
||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||
mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path
|
||||
|
||||
# Kill the app to force it to restart the mounted APK in case it's currently running.
|
||||
$KILL
|
||||
""".trimIndent()
|
||||
}
|
||||
Reference in New Issue
Block a user