grape/app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt

389 lines
14 KiB
Kotlin

package com.android.grape.sai
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Pair
import com.android.grape.MainApplication
import com.android.grape.R
import com.android.grape.sai.inter.ApkSource
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
import com.android.grape.sai.param.SaiPiSessionStatus
import com.android.grape.sai.prefers.DbgPreferencesHelper
import com.android.grape.sai.prefers.PreferencesHelper
import com.android.grape.sai.rootless.AndroidPackageInstallerError
import com.android.grape.sai.shell.MiuiUtils
import com.android.grape.sai.shell.Shell
import com.android.grape.util.Util
import com.blankj.utilcode.util.LogUtils
import java.io.File
import java.util.Arrays
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
BaseSaiPackageInstaller(c!!) {
private val mAwaitingBroadcast = AtomicBoolean(false)
private val mExecutor: ExecutorService = Executors.newFixedThreadPool(4)
private val mWorkerThread = HandlerThread("RootlessSaiPi Worker")
private val mWorkerHandler: Handler
private var mCurrentSessionId: String? = null
//TODO read package from apk stream, this is too potentially inconsistent
private val mPackageInstalledBroadcastReceiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(tag(), intent.toString())
if (!mAwaitingBroadcast.get()) return
mAwaitingBroadcast.set(false)
val installedPackage: String
try {
installedPackage = intent.dataString!!.replace("package:", "")
val installerPackage: String =
context.getPackageManager().getInstallerPackageName(installedPackage)?:""
Log.d(tag(), "installerPackage=$installerPackage")
if ("com.android.grape" != installerPackage) return
} catch (e: Exception) {
Log.wtf(tag(), e)
return
}
mCurrentSessionId?.let {
setSessionState(
it,
SaiPiSessionState.Builder(
it,
SaiPiSessionStatus.INSTALLATION_SUCCEED
).packageName(
installedPackage
).resolvePackageMeta(MainApplication.instance).build()
)
}
unlockInstallation()
// Toast.makeText(context,"Installation succeed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(true)
}
}
init {
mWorkerThread.start()
mWorkerHandler = Handler(mWorkerThread.looper)
val packageAddedFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
packageAddedFilter.addDataScheme("package")
MainApplication.instance.registerReceiver(
mPackageInstalledBroadcastReceiver,
packageAddedFilter,
null,
mWorkerHandler
)
}
override fun enqueueSession(sessionId: String) {
val params: SaiPiSessionParams? = takeCreatedSession(sessionId)
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName(
params?.apkSource()?.appName
).build()
)
mExecutor.submit {
params?.let {
install(sessionId, it)
}
}
}
private fun install(sessionId: String, params: SaiPiSessionParams) {
lockInstallation(sessionId)
val appTempName: String? = params.apkSource().appName
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName).build()
)
var androidSessionId: Int? = null
try {
params.apkSource().use { apkSource ->
if (!shell.isAvailable()) {
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).error(
MainApplication.instance.getString(
R.string.installer_error_shell,
installerName,
shellUnavailableMessage
), null
).build()
)
unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false)
return
}
androidSessionId = createSession()
val path = "/sdcard/apks/" + "com.zhiliaoapp.musically"
val file = File(path)
val files = file.listFiles()
var currentApkFile = 0
for (f in files) {
if (f.length() <= 0) {
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(
appTempName
).error(
MainApplication.instance.getString(R.string.installer_error_unknown_apk_size),
null
).build()
)
unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false)
return
}
ensureCommandSucceeded(
shell.exec(
Shell.Command(
"pm",
"install-write",
f.length().toString(),
androidSessionId.toString(),
String.format("%d.apk", currentApkFile++),
f.path
)
)
)
}
mAwaitingBroadcast.set(true)
val installationResult: Shell.Result =
shell.exec(Shell.Command("pm", "install-commit", androidSessionId.toString()))
Log.i(tag(), "installationResult:" + installationResult.isSuccessful)
if (!installationResult.isSuccessful) {
mAwaitingBroadcast.set(false)
val shortError: String = MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(apkSource)}
${parseError(installationResult)}
""".trimIndent()
)
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(appTempName)
.error(
shortError, """
$shortError
${installationResult.toString()}
""".trimIndent()
)
.build()
)
unlockInstallation()
Util.setInstallRet(false)
} else {
Util.isClickRet = true
}
}
} catch (e: Exception) {
//TODO this catches resources close exception causing a crash, same in rootless installer
Log.w(tag(), e)
if (androidSessionId != null) {
shell.exec(Shell.Command("pm", "install-abandon", androidSessionId.toString()))
}
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(appTempName)
.error(
MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(params.apkSource())}
${e.localizedMessage}
""".trimIndent()
), MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(params.apkSource())}
${Utils.throwableToString(e)}
""".trimIndent()
)
)
.build()
)
unlockInstallation()
Util.setInstallRet(false)
}
}
private fun lockInstallation(sessionId: String) {
try {
mSharedSemaphore.acquire()
} catch (e: InterruptedException) {
throw RuntimeException("wtf", e)
}
mCurrentSessionId = sessionId
}
private fun unlockInstallation() {
mSharedSemaphore.release()
}
private fun ensureCommandSucceeded(result: Shell.Result): String {
if (!result.isSuccessful) throw RuntimeException(result.toString())
return result.out
}
private fun getSessionInfo(apkSource: ApkSource): String {
var saiVersion = "???"
try {
saiVersion = MainApplication.instance.getPackageManager()
.getPackageInfo(MainApplication.instance.getPackageName(), 0).versionName?:""
} catch (e: PackageManager.NameNotFoundException) {
Log.wtf(tag(), "Unable to get SAI version", e)
}
return java.lang.String.format(
"%s: %s %s | %s | Android %s | Using %s ApkSource implementation | SAI %s",
MainApplication.instance.getString(R.string.installer_device),
Build.BRAND,
Build.MODEL,
if (MiuiUtils.isMiui) "MIUI" else "Not MIUI",
Build.VERSION.RELEASE,
apkSource.javaClass.getSimpleName(),
saiVersion
)
}
@Throws(RuntimeException::class)
private fun createSession(): Int {
val installLocation: String = java.lang.String.valueOf(
PreferencesHelper.getInstance(MainApplication.instance).installLocation
)
val commandsToAttempt: ArrayList<Shell.Command> = ArrayList<Shell.Command>()
val customInstallCreateCommand: String? =
DbgPreferencesHelper.customInstallCreateCommand
if (customInstallCreateCommand != null) {
val args = ArrayList(
Arrays.asList(
*customInstallCreateCommand.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()))
val command = args.removeAt(0)
commandsToAttempt.add(Shell.Command(command, *args.toTypedArray()))
LogUtils.d(tag(), "Using custom install-create command: $customInstallCreateCommand")
} else {
commandsToAttempt.add(
Shell.Command(
"pm",
"install-create",
"-r",
"--install-location",
installLocation,
"-i",
shell.makeLiteral("com.android.grape")
)
)
commandsToAttempt.add(
Shell.Command(
"pm",
"install-create",
"-r",
"-i",
shell.makeLiteral("com.android.grape")
)
)
}
val attemptedCommands: MutableList<Pair<Shell.Command, String>> =
ArrayList<Pair<Shell.Command, String>>()
for (commandToAttempt in commandsToAttempt) {
val result: Shell.Result = shell.exec(commandToAttempt)
attemptedCommands.add(Pair(commandToAttempt, result.toString()))
if (!result.isSuccessful) {
Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result))
continue
}
val sessionId = extractSessionId(result.out)
if (sessionId != null) return sessionId
else Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result))
}
val exceptionMessage = StringBuilder("Unable to create session, attempted commands: ")
var i = 1
for (attemptedCommand in attemptedCommands) {
exceptionMessage.append("\n\n").append(i++).append(") ==========================\n")
.append(attemptedCommand.first)
.append("\nVVVVVVVVVVVVVVVV\n")
.append(attemptedCommand.second)
}
exceptionMessage.append("\n")
throw IllegalStateException(exceptionMessage.toString())
}
private fun extractSessionId(commandResult: String): Int? {
try {
val sessionIdPattern = Pattern.compile("(\\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(commandResult)
sessionIdMatcher.find()
return sessionIdMatcher.group(1).toInt()
} catch (e: Exception) {
Log.w(tag(), commandResult, e)
return null
}
}
private fun parseError(installCommitResult: Shell.Result): String {
var matchedError: AndroidPackageInstallerError = AndroidPackageInstallerError.UNKNOWN
for (error in AndroidPackageInstallerError.values()) {
if (installCommitResult.out.contains(error.error)) {
matchedError = error
break
}
}
return matchedError.getDescription(MainApplication.instance)
}
protected abstract val shell: Shell
protected abstract val installerName: String?
protected abstract val shellUnavailableMessage: String?
abstract override fun tag(): String?
companion object {
private val mSharedSemaphore = Semaphore(1)
}
}