389 lines
14 KiB
Kotlin
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)
|
|
}
|
|
} |