如何使用 "libsu" 库(或 adb)在 Android Q 上安装拆分 APK 文件?
How to use "libsu" library (or adb) to install split APK files on Android Q?
背景
使用 root,我知道对于单个 APK 文件,我们可以使用 "libsu" 库 (here) 来安装:
val installResult = Shell.su("pm install -t \"$filePath\"").exec()
如果失败了(在新的 Android 版本上失败,不确定是从哪个版本开始的),那么(关于这个 的文章):
val installResult = Shell.su("cat \"$filePath\" | pm install -t -S ${apkSource.fileSize}").exec()
我也知道在安装拆分 APK 文件时事情变得非常混乱(如图 here 所示)。首先你需要创建一个会话,使用"pm install-create"命令:
var sessionId: Int? = null
run {
val sessionIdResult =
Shell.su("pm install-create -r -t").exec().out
val sessionIdPattern = Pattern.compile("(\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(sessionIdResult[0])
sessionIdMatcher.find()
sessionId = Integer.parseInt(sessionIdMatcher.group(1)!!)
Log.d("AppLog", "sessionId:$sessionId")
}
然后您必须 "push" 每个 APK 文件,例如:
for (apkSource in fileInfoList) {
val filePath = File(apkSource.parentFilePath, apkSource.fileName).absolutePath
Log.d("AppLog", "installing APK : $filePath ${apkSource.fileSize} ")
val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()
Log.d("AppLog", "success pushing apk:${apkSource.fileName} ? ${result.isSuccess}")
}
然后使用 pm install-commit
提交更改:
val installResult = Shell.su("pm install-commit $sessionId").exec()
有关这一切的文档:
install-create [-lrtsfdg] [-i PACKAGE] [--user USER_ID|all|current]
[-p INHERIT_PACKAGE] [--install-location 0/1/2]
[--install-reason 0/1/2/3/4] [--originating-uri URI]
[--referrer URI] [--abi ABI_NAME] [--force-sdk]
[--preload] [--instantapp] [--full] [--dont-kill]
[--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]
[--multi-package] [--staged]
Like "install", but starts an install session. Use "install-write"
to push data into the session, and "install-commit" to finish.
install-write [-S BYTES] SESSION_ID SPLIT_NAME [PATH|-]
Write an apk into the given install session. If the path is '-', data
will be read from stdin. Options are:
-S: size in bytes of package, required for stdin
install-commit SESSION_ID
Commit the given active install session, installing the app.
问题
在 Android P 之前一切正常,但由于某种原因,它在 Q beta 6 上失败了,向我显示了这个错误:
avc: denied { read } for scontext=u:r:system_server:s0 tcontext=u:object_r:sdcardfs:s0 tclass=file permissive=0
System server has no access to read file context u:object_r:sdcardfs:s0 (from path /storage/emulated/0/Download/split/base.apk, context u:r:system_server:s0)
Error: Unable to open file: /storage/emulated/0/Download/split/base.apk
Consider using a file under /data/local/tmp/
我试过的
这与我发现的单个 APK 的情况相似,,所以我认为也许在这里也可以应用类似的解决方案:
val result = Shell.su("cat $filePath | pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()
这仍然只适用于 Android P 及以下。
所以看到我查看的原始代码有效,它使用了 InputStream,正如文档所暗示的那样,这是可能的。这是他们拥有的:
while (apkSource.nextApk())
ensureCommandSucceeded(Root.exec(String.format("pm install-write -S %d %d \"%s\"", apkSource.getApkLength(), sessionId, apkSource.getApkName()), apkSource.openApkInputStream()));
所以我尝试的是这样的:
val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" -")
.add(SuFileInputStream(filePath)).exec()
很遗憾,这也没有用。
问题
我知道我可以只复制相同的代码,但还有没有办法改用该库(因为它会更短、更优雅)?如果可以,我该怎么做?
它很乱,但试试这个代码。它使用 SuFileInputStream 读取 apk 文件内容,然后将其通过管道传输到 install-write 命令中。这在理论上应该可以解决问题。
// getting session id
val createSessionResult = Shell.su("pm install-create -S $size").
val sessionIdRegex = "\[([0-9]+)]".toRegex()
var sessionId: Int? = null
for (line in createSessionResult.out) {
val result = sessionIdRegex.find(line)?.groupValues?.get(1)?.toInt()
if (result != null) {
sessionId = result
break
}
}
// writing apks, you might want to extract this to another function
val writeShellInStream = PipedInputStream()
PipedOutputStream(writeShellInStream).use { writeShellInOutputStream ->
PrintWriter(writeShellInOutputStream).use { writeShellInWriter ->
writeShellInWriter.println("pm install-write -S $size $sessionId base") // eventually replace base with split apk name
writeShellInWriter.flush()
Shell.su(writeShellInStream).submit { writeResult ->
if (writeResult.isSuccess) {
Shell.su("pm install-commit $sessionId").submit { commitResult ->
// commitResult.isSuccess to check if worked
}
}
}
apkInputStream.copyTo(writeShellInOutputStream)
writeShellInWriter.println()
}
}
编辑:如果您不需要从流安装,您可能想先尝试命令 "cat [your apk file] | pm install-write -S [size] [sessionId] [base / split apk name]"。如果 cat 不起作用,请尝试 "dd if=[apk file]"。
好吧,我不知道如何使用这个库来安装 split-apk,但这里有一段似乎可以使用不同库的简短代码:
build.gradle
//https://github.com/topjohnwu/libsu
implementation "com.github.topjohnwu.libsu:core:2.5.1"
single/split apk 文件的基础 class:
open class FileInfo(val name: String, val fileSize: Long, val file: File? = null) {
open fun getInputStream(): InputStream = if (file!= null) FileInputStream(file) else throw NotImplementedError("need some way to create InputStream")
}
获取 root 并安装:
Shell.getShell {
val isRoot = it.isRoot
Log.d("AppLog", "isRoot ?$isRoot ")
AsyncTask.execute {
val apkFilesPath = "/storage/emulated/0/Download/split/"
val fileInfoList = getFileInfoList(apkFilesPath)
installSplitApkFiles(fileInfoList)
}
}
安装本身:
@WorkerThread
private fun installSplitApkFiles(apkFiles: ArrayList<FileInfo>): Boolean {
if (apkFiles.size == 1) {
//single file that we can actually reach, so use normal method
val apkFile = apkFiles[0]
if (apkFiles[0].apkFile != null) {
Log.d("AppLog", "Installing a single APK ${apkFile.name} ${apkFile.fileSize} ")
val installResult = Shell.su("cat \"${apkFile.apkFile!!.absolutePath}\" | pm install -t -S ${apkFile.fileSize}").exec()
Log.d("AppLog", "succeeded installing?${installResult.isSuccess}")
if (installResult.isSuccess)
return true
}
}
var sessionId: Int? = null
Log.d("AppLog", "installing split apk files:$apkFiles")
run {
val sessionIdResult = Shell.su("pm install-create -r -t").exec().out
// Note: might need to use these instead:
// "pm install-create -r --install-location 0 -i '${BuildConfig.APPLICATION_ID}'"
// "pm install-create -r -i '${BuildConfig.APPLICATION_ID}'"
val sessionIdPattern = Pattern.compile("(\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(sessionIdResult[0])
sessionIdMatcher.find()
sessionId = Integer.parseInt(sessionIdMatcher.group(1)!!)
// Log.d("AppLog", "sessionId:$sessionId")
}
for (apkFile in apkFiles) {
Log.d("AppLog", "installing APK : ${apkFile.name} ${apkFile.fileSize} ")
// pm install-write [-S BYTES] SESSION_ID SPLIT_NAME [PATH]
val command = arrayOf("su", "-c", "pm", "install-write", "-S", "${apkFile.fileSize}", "$sessionId", apkFile.name)
val process: Process = Runtime.getRuntime().exec(command)
val inputPipe = apkFile.getInputStream()
try {
process.outputStream.use { outputStream -> inputPipe.copyTo(outputStream) }
} catch (e: java.lang.Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) process.destroyForcibly() else process.destroy()
throw RuntimeException(e)
}
process.waitFor()
val inputStr = process.inputStream.readBytes().toString(Charset.defaultCharset())
val errStr = process.errorStream.readBytes().toString(Charset.defaultCharset())
val isSucceeded = process.exitValue() == 0
Log.d("AppLog", "isSucceeded?$isSucceeded inputStr:$inputStr errStr:$errStr")
}
// "pm install-commit %d ", sessionId
Log.d("AppLog", "committing...")
val installResult = Shell.su("pm install-commit $sessionId").exec()
Log.d("AppLog", "succeeded installing?${installResult.isSuccess}")
return installResult.isSuccess
}
以获取拆分apk文件列表为例:
fun SimpleDateFormat.tryParse(str: String) = try {
parse(str) != null
} catch (e: Exception) {
false
}
@WorkerThread
private fun getFileInfoList(splitApkPath: String): ArrayList<FileInfo> {
val parentFile = File(splitApkPath)
val result = ArrayList<FileInfo>()
if (parentFile.exists() && parentFile.canRead()) {
val listFiles = parentFile.listFiles() ?: return ArrayList()
for (file in listFiles)
result.add(FileInfo(file.name, file.length(), file))
return result
}
val longLines = Shell.su("ls -l $splitApkPath").exec().out
val pattern = Pattern.compile(" +")
val formatter = SimpleDateFormat("HH:mm", Locale.getDefault())
longLinesLoop@ for (line in longLines) {
// Log.d("AppLog", "line:$line")
val matcher = pattern.matcher(line)
for (i in 0 until 4)
if (!matcher.find())
continue@longLinesLoop
//got to file size
val startSizeStr = matcher.end()
matcher.find()
val endSizeStr = matcher.start()
val fileSizeStr = line.substring(startSizeStr, endSizeStr)
while (true) {
val testTimeStr: String =
line.substring(matcher.end(), line.indexOf(' ', matcher.end()))
if (formatter.tryParse(testTimeStr)) {
//found time, so apk is next
val fileName = line.substring(line.indexOf(' ', matcher.end()) + 1)
if (fileName.endsWith("apk"))
// Log.d("AppLog", "fileSize:$fileSizeStr fileName:$fileName")
result.add(FileInfo(fileName, fileSizeStr.toLong(), File(splitApkPath, fileName)))
break
}
matcher.find()
}
}
// Log.d("AppLog", "result:${result.size}")
return result
}
背景
使用 root,我知道对于单个 APK 文件,我们可以使用 "libsu" 库 (here) 来安装:
val installResult = Shell.su("pm install -t \"$filePath\"").exec()
如果失败了(在新的 Android 版本上失败,不确定是从哪个版本开始的),那么(关于这个
val installResult = Shell.su("cat \"$filePath\" | pm install -t -S ${apkSource.fileSize}").exec()
我也知道在安装拆分 APK 文件时事情变得非常混乱(如图 here 所示)。首先你需要创建一个会话,使用"pm install-create"命令:
var sessionId: Int? = null
run {
val sessionIdResult =
Shell.su("pm install-create -r -t").exec().out
val sessionIdPattern = Pattern.compile("(\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(sessionIdResult[0])
sessionIdMatcher.find()
sessionId = Integer.parseInt(sessionIdMatcher.group(1)!!)
Log.d("AppLog", "sessionId:$sessionId")
}
然后您必须 "push" 每个 APK 文件,例如:
for (apkSource in fileInfoList) {
val filePath = File(apkSource.parentFilePath, apkSource.fileName).absolutePath
Log.d("AppLog", "installing APK : $filePath ${apkSource.fileSize} ")
val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()
Log.d("AppLog", "success pushing apk:${apkSource.fileName} ? ${result.isSuccess}")
}
然后使用 pm install-commit
提交更改:
val installResult = Shell.su("pm install-commit $sessionId").exec()
有关这一切的文档:
install-create [-lrtsfdg] [-i PACKAGE] [--user USER_ID|all|current]
[-p INHERIT_PACKAGE] [--install-location 0/1/2]
[--install-reason 0/1/2/3/4] [--originating-uri URI]
[--referrer URI] [--abi ABI_NAME] [--force-sdk]
[--preload] [--instantapp] [--full] [--dont-kill]
[--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]
[--multi-package] [--staged]
Like "install", but starts an install session. Use "install-write"
to push data into the session, and "install-commit" to finish.
install-write [-S BYTES] SESSION_ID SPLIT_NAME [PATH|-]
Write an apk into the given install session. If the path is '-', data
will be read from stdin. Options are:
-S: size in bytes of package, required for stdin
install-commit SESSION_ID
Commit the given active install session, installing the app.
问题
在 Android P 之前一切正常,但由于某种原因,它在 Q beta 6 上失败了,向我显示了这个错误:
avc: denied { read } for scontext=u:r:system_server:s0 tcontext=u:object_r:sdcardfs:s0 tclass=file permissive=0
System server has no access to read file context u:object_r:sdcardfs:s0 (from path /storage/emulated/0/Download/split/base.apk, context u:r:system_server:s0)
Error: Unable to open file: /storage/emulated/0/Download/split/base.apk
Consider using a file under /data/local/tmp/
我试过的
这与我发现的单个 APK 的情况相似,
val result = Shell.su("cat $filePath | pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()
这仍然只适用于 Android P 及以下。
所以看到我查看的原始代码有效,它使用了 InputStream,正如文档所暗示的那样,这是可能的。这是他们拥有的:
while (apkSource.nextApk())
ensureCommandSucceeded(Root.exec(String.format("pm install-write -S %d %d \"%s\"", apkSource.getApkLength(), sessionId, apkSource.getApkName()), apkSource.openApkInputStream()));
所以我尝试的是这样的:
val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" -")
.add(SuFileInputStream(filePath)).exec()
很遗憾,这也没有用。
问题
我知道我可以只复制相同的代码,但还有没有办法改用该库(因为它会更短、更优雅)?如果可以,我该怎么做?
它很乱,但试试这个代码。它使用 SuFileInputStream 读取 apk 文件内容,然后将其通过管道传输到 install-write 命令中。这在理论上应该可以解决问题。
// getting session id
val createSessionResult = Shell.su("pm install-create -S $size").
val sessionIdRegex = "\[([0-9]+)]".toRegex()
var sessionId: Int? = null
for (line in createSessionResult.out) {
val result = sessionIdRegex.find(line)?.groupValues?.get(1)?.toInt()
if (result != null) {
sessionId = result
break
}
}
// writing apks, you might want to extract this to another function
val writeShellInStream = PipedInputStream()
PipedOutputStream(writeShellInStream).use { writeShellInOutputStream ->
PrintWriter(writeShellInOutputStream).use { writeShellInWriter ->
writeShellInWriter.println("pm install-write -S $size $sessionId base") // eventually replace base with split apk name
writeShellInWriter.flush()
Shell.su(writeShellInStream).submit { writeResult ->
if (writeResult.isSuccess) {
Shell.su("pm install-commit $sessionId").submit { commitResult ->
// commitResult.isSuccess to check if worked
}
}
}
apkInputStream.copyTo(writeShellInOutputStream)
writeShellInWriter.println()
}
}
编辑:如果您不需要从流安装,您可能想先尝试命令 "cat [your apk file] | pm install-write -S [size] [sessionId] [base / split apk name]"。如果 cat 不起作用,请尝试 "dd if=[apk file]"。
好吧,我不知道如何使用这个库来安装 split-apk,但这里有一段似乎可以使用不同库的简短代码:
build.gradle
//https://github.com/topjohnwu/libsu
implementation "com.github.topjohnwu.libsu:core:2.5.1"
single/split apk 文件的基础 class:
open class FileInfo(val name: String, val fileSize: Long, val file: File? = null) {
open fun getInputStream(): InputStream = if (file!= null) FileInputStream(file) else throw NotImplementedError("need some way to create InputStream")
}
获取 root 并安装:
Shell.getShell {
val isRoot = it.isRoot
Log.d("AppLog", "isRoot ?$isRoot ")
AsyncTask.execute {
val apkFilesPath = "/storage/emulated/0/Download/split/"
val fileInfoList = getFileInfoList(apkFilesPath)
installSplitApkFiles(fileInfoList)
}
}
安装本身:
@WorkerThread
private fun installSplitApkFiles(apkFiles: ArrayList<FileInfo>): Boolean {
if (apkFiles.size == 1) {
//single file that we can actually reach, so use normal method
val apkFile = apkFiles[0]
if (apkFiles[0].apkFile != null) {
Log.d("AppLog", "Installing a single APK ${apkFile.name} ${apkFile.fileSize} ")
val installResult = Shell.su("cat \"${apkFile.apkFile!!.absolutePath}\" | pm install -t -S ${apkFile.fileSize}").exec()
Log.d("AppLog", "succeeded installing?${installResult.isSuccess}")
if (installResult.isSuccess)
return true
}
}
var sessionId: Int? = null
Log.d("AppLog", "installing split apk files:$apkFiles")
run {
val sessionIdResult = Shell.su("pm install-create -r -t").exec().out
// Note: might need to use these instead:
// "pm install-create -r --install-location 0 -i '${BuildConfig.APPLICATION_ID}'"
// "pm install-create -r -i '${BuildConfig.APPLICATION_ID}'"
val sessionIdPattern = Pattern.compile("(\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(sessionIdResult[0])
sessionIdMatcher.find()
sessionId = Integer.parseInt(sessionIdMatcher.group(1)!!)
// Log.d("AppLog", "sessionId:$sessionId")
}
for (apkFile in apkFiles) {
Log.d("AppLog", "installing APK : ${apkFile.name} ${apkFile.fileSize} ")
// pm install-write [-S BYTES] SESSION_ID SPLIT_NAME [PATH]
val command = arrayOf("su", "-c", "pm", "install-write", "-S", "${apkFile.fileSize}", "$sessionId", apkFile.name)
val process: Process = Runtime.getRuntime().exec(command)
val inputPipe = apkFile.getInputStream()
try {
process.outputStream.use { outputStream -> inputPipe.copyTo(outputStream) }
} catch (e: java.lang.Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) process.destroyForcibly() else process.destroy()
throw RuntimeException(e)
}
process.waitFor()
val inputStr = process.inputStream.readBytes().toString(Charset.defaultCharset())
val errStr = process.errorStream.readBytes().toString(Charset.defaultCharset())
val isSucceeded = process.exitValue() == 0
Log.d("AppLog", "isSucceeded?$isSucceeded inputStr:$inputStr errStr:$errStr")
}
// "pm install-commit %d ", sessionId
Log.d("AppLog", "committing...")
val installResult = Shell.su("pm install-commit $sessionId").exec()
Log.d("AppLog", "succeeded installing?${installResult.isSuccess}")
return installResult.isSuccess
}
以获取拆分apk文件列表为例:
fun SimpleDateFormat.tryParse(str: String) = try {
parse(str) != null
} catch (e: Exception) {
false
}
@WorkerThread
private fun getFileInfoList(splitApkPath: String): ArrayList<FileInfo> {
val parentFile = File(splitApkPath)
val result = ArrayList<FileInfo>()
if (parentFile.exists() && parentFile.canRead()) {
val listFiles = parentFile.listFiles() ?: return ArrayList()
for (file in listFiles)
result.add(FileInfo(file.name, file.length(), file))
return result
}
val longLines = Shell.su("ls -l $splitApkPath").exec().out
val pattern = Pattern.compile(" +")
val formatter = SimpleDateFormat("HH:mm", Locale.getDefault())
longLinesLoop@ for (line in longLines) {
// Log.d("AppLog", "line:$line")
val matcher = pattern.matcher(line)
for (i in 0 until 4)
if (!matcher.find())
continue@longLinesLoop
//got to file size
val startSizeStr = matcher.end()
matcher.find()
val endSizeStr = matcher.start()
val fileSizeStr = line.substring(startSizeStr, endSizeStr)
while (true) {
val testTimeStr: String =
line.substring(matcher.end(), line.indexOf(' ', matcher.end()))
if (formatter.tryParse(testTimeStr)) {
//found time, so apk is next
val fileName = line.substring(line.indexOf(' ', matcher.end()) + 1)
if (fileName.endsWith("apk"))
// Log.d("AppLog", "fileSize:$fileSizeStr fileName:$fileName")
result.add(FileInfo(fileName, fileSizeStr.toLong(), File(splitApkPath, fileName)))
break
}
matcher.find()
}
}
// Log.d("AppLog", "result:${result.size}")
return result
}