Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import app.revanced.library.installation.installer.Installer.Apk
import java.io.Closeable
import java.io.File

Expand All @@ -26,7 +25,7 @@ import java.io.File
class LocalInstaller(
private val context: Context,
onResult: (result: LocalInstallerResult) -> Unit,
) : Installer<Unit, Installation>(), Closeable {
) : Installer<Unit, Installation, InstallOptions>(), Closeable {
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val pmStatus = intent.getIntExtra(LocalInstallerService.EXTRA_STATUS, -999)
Expand Down Expand Up @@ -56,13 +55,14 @@ class LocalInstaller(
)
}

override suspend fun install(apk: Apk) {
logger.info("Installing ${apk.file.name}")
override suspend fun install(options: InstallOptions) {
val patchedApk = options.patchedApk
logger.info("Installing ${patchedApk.file.name}")

val packageInstaller = context.packageManager.packageInstaller

packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
session.writeApk(apk.file)
session.writeApk(patchedApk.file)
session.commit(intentSender)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ class AdbShellCommandRunner : ShellCommandRunner {
override val output by lazy { process.inputStream.bufferedReader().readText().removeSuffix("\n") }
override val error by lazy { process.errorStream.bufferedReader().readText() }

override fun waitFor() {
override fun waitFor(): RunResult {
process.waitFor()
return this
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ interface RunResult {
/**
* Waits for the command to finish.
*/
fun waitFor() {}
fun waitFor(): RunResult = this
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import se.vidstige.jadb.managers.PackageManager.UPDATE_OWNERSHIP
*/
class AdbInstaller(
deviceSerial: String? = null,
) : Installer<AdbInstallerResult, Installation>() {
) : Installer<AdbInstallerResult, Installation, InstallOptions>() {
private val shellCommandRunner: ShellCommandRunner
private val packageManager: PackageManager

Expand All @@ -30,11 +30,13 @@ class AdbInstaller(
logger.fine("Connected to $deviceSerial")
}

override suspend fun install(apk: Apk): AdbInstallerResult {
override suspend fun install(options: InstallOptions): AdbInstallerResult {
val patchedApk = options.patchedApk

return runPackageManager {
val sdkVersion = shellCommandRunner(GET_SDK_VERSION).output.toInt()
if (sdkVersion < 34) install(apk.file)
else installWithOptions(apk.file, listOf(UPDATE_OWNERSHIP))
if (sdkVersion < 34) install(patchedApk.file)
else installWithOptions(patchedApk.file, listOf(UPDATE_OWNERSHIP))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ internal object Constants {
const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER"
const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH"
const val GET_SDK_VERSION = "getprop ro.build.version.sdk"

const val UNINSTALL_KEEP_DATA = "pm uninstall -k --user 0 $PLACEHOLDER"
const val INSTALL_STOCK_APK = "pm install -r -d --user 0 $PLACEHOLDER"
const val GET_INSTALLED_VERSION_CODE =
"dumpsys package $PLACEHOLDER | sed -n 's/.*versionCode=\\([0-9]*\\).*/\\1/p' | head -n 1"
const val MOUNT_APK =
"base_path=\"$MOUNTED_APK_PATH\" && " +
$$"mv $$TMP_FILE_PATH $base_path && " +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package app.revanced.library.installation.installer

import app.revanced.library.installation.installer.Installer.Apk
import java.io.File
import java.util.logging.Logger

Expand All @@ -10,7 +9,7 @@ import java.util.logging.Logger
* @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() {
abstract class Installer<TInstallerResult, TInstallation : Installation, TInstallOptions : InstallOptions> internal constructor() {
/**
* The [Logger].
*/
Expand All @@ -19,11 +18,11 @@ abstract class Installer<TInstallerResult, TInstallation : Installation> interna
/**
* Installs the [Apk] file.
*
* @param apk The [Apk] file.
* @param options The [InstallOptions].
*
* @return The result of the installation.
*/
abstract suspend fun install(apk: Apk): TInstallerResult
abstract suspend fun install(options: TInstallOptions): TInstallerResult

/**
* Uninstalls the package.
Expand All @@ -43,11 +42,5 @@ abstract class Installer<TInstallerResult, TInstallation : Installation> interna
*/
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)
open class Apk(val file: File)
}
Comment thread
Ushie marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package app.revanced.library.installation.installer

open class InstallOptions(
val patchedApk: Installer.Apk
Comment thread
Ushie marked this conversation as resolved.
Outdated
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ 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.GET_INSTALLED_VERSION_CODE
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.INSTALL_STOCK_APK
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
Expand All @@ -16,8 +18,6 @@ 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

/**
Expand All @@ -30,7 +30,7 @@ import java.io.File
@Suppress("MemberVisibilityCanBePrivate")
abstract class RootInstaller internal constructor(
shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner,
) : Installer<RootInstallerResult, RootInstallation>() {
) : Installer<RootInstallerResult, RootInstallation, RootInstallOptions>() {

/**
* The command runner used to run commands on the device.
Expand All @@ -43,19 +43,41 @@ abstract class RootInstaller internal constructor(
}

/**
* Installs the given [apk] by mounting.
* Installs the given APK by mounting it over a stock installation.
Comment thread
Ushie marked this conversation as resolved.
Outdated
*
* @param apk The [Apk] to install.
* The stock APK is used to ensure a valid base for mounting. If the currently
* installed version differs from the expected stock version, the stock APK is
* installed first. If the installed version is newer than the expected version,
* reinstallation may be required.
*
* @throws PackageNameRequiredException If the [Apk] does not have a package name.
* @param options The install options containing the patched APK to mount and the
* stock APK to use as the installation base.
*
* @throws PackageDowngradeRequiredException If the installed app version is
* newer than the expected stock version.
*/
override suspend fun install(apk: Apk): RootInstallerResult {
logger.info("Installing ${apk.packageName} by mounting")
override suspend fun install(options: RootInstallOptions): RootInstallerResult {
val patchedApk = options.patchedApk
val stockApk = options.stockApk
val packageName = stockApk.packageName
val expectedVersionCode = stockApk.versionCode
Comment thread
Ushie marked this conversation as resolved.
Outdated
logger.info("Installing $packageName by mounting")

// Ensure the installed base app matches the stock APK version.
val installedVersionCode = getInstalledVersionCode(packageName)
Comment thread
Ushie marked this conversation as resolved.
Outdated

if (installedVersionCode != null && installedVersionCode > expectedVersionCode) {
throw PackageDowngradeRequiredException(packageName)
}

val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException()
if (installedVersionCode != expectedVersionCode) {
logger.info("Installing stock APK for $packageName")
INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor()
}
packageName.assertInstalled()
Comment thread
Ushie marked this conversation as resolved.

// Setup files.
apk.file.move(TMP_FILE_PATH)
patchedApk.file.move(TMP_FILE_PATH)
CREATE_INSTALLATION_PATH().waitFor()
MOUNT_APK(packageName)().waitFor()

Expand Down Expand Up @@ -88,7 +110,7 @@ abstract class RootInstaller internal constructor(
val patchedApkPath = MOUNTED_APK_PATH(packageName)

val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0
if (patchedApkExists) return null
if (!patchedApkExists) return null
Comment thread
Ushie marked this conversation as resolved.

return RootInstallation(
INSTALLED_APK_PATH(packageName)().output.ifEmpty { null },
Expand All @@ -114,7 +136,8 @@ abstract class RootInstaller internal constructor(
*
* @param content The content of the file.
*/
protected fun String.write(content: String) = shellCommandRunner.write(content.byteInputStream(), this)
protected fun String.write(content: String) =
shellCommandRunner.write(content.byteInputStream(), this)

/**
* Asserts that the package is installed.
Expand All @@ -127,8 +150,20 @@ abstract class RootInstaller internal constructor(
}
}

internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : Exception("Failed to resolve installed APK path for package \"$packageName\"")
private fun getInstalledVersionCode(packageName: String): Int? {
val result = GET_INSTALLED_VERSION_CODE(packageName)().waitFor()

if (result.exitCode != 0) return null

return result.output.trim().toIntOrNull()
Comment thread
Ushie marked this conversation as resolved.
Outdated
}

internal class PackageDowngradeRequiredException internal constructor(packageName: String) :
Exception("$packageName requires reinstallation for a downgrade, data wipe is likely")

internal class FailedToFindInstalledPackageException internal constructor(packageName: String) :
Exception("Failed to resolve installed APK path for package \"$packageName\"")

internal class PackageNameRequiredException internal constructor() : Exception("Package name is required")
internal class NoRootPermissionException internal constructor() : Exception("No root permission")
internal class NoRootPermissionException internal constructor() :
Exception("No root permission")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.revanced.library.installation.installer

import java.io.File

class RootInstallOptions(
patchedApk: Installer.Apk,
val stockApk: StockApk
) : InstallOptions(patchedApk)

class StockApk(file: File, val packageName: String, val versionCode: Int) : Installer.Apk(file)
Comment thread
Ushie marked this conversation as resolved.
Outdated
Loading