如何使用“libsu"库(或 adb)在 Android Q 上安装拆分的 APK 文件? [英] How to use "libsu" library (or adb) to install split APK files on Android Q?

查看:33
本文介绍了如何使用“libsu"库(或 adb)在 Android Q 上安装拆分的 APK 文件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

使用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 文件时事情变得非常混乱(如图所示 此处).首先你需要创建一个会话,使用pm install-create"命令:

var sessionId: Int?= 空跑 {val sessionIdResult =Shell.su("pm install-create -r -t").exec().outval sessionIdPattern = Pattern.compile("(\\d+)")val sessionIdMatcher = sessionIdPattern.matcher(sessionIdResult[0])sessionIdMatcher.find()sessionId = Integer.parseInt(sessionIdMatcher.group(1)!!)Log.d("AppLog", "sessionId:$sessionId")}

然后您必须推送"每个 APK 文件,例如:

for (fileInfoList 中的 apkSource) {val filePath = File(apkSource.parentFilePath, apkSource.fileName).absolutePathLog.d("AppLog", "安装 APK : $filePath ${apkSource.fileSize} ")val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()Log.d("AppLog", "成功推送 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]类似于安装",但会启动安装会话.使用安装-写入"将数据推送到会话中,然后安装提交"完成.安装写入 [-S BYTES] SESSION_ID SPLIT_NAME [PATH|-]将 apk 写入给定的安装会话.如果路径是'-',数据将从标准输入读取.选项是:-S:包的字节大小,标准输入需要安装提交 SESSION_ID提交给定的活动安装会话,安装应用程序.

问题

这一切在 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系统服务器无权读取文件上下文 u:object_r:sdcardfs:s0(来自路径/storage/emulated/0/Download/split/base.apk,上下文 u:r:system_server:s0)错误:无法打开文件:/storage/emulated/0/Download/split/base.apk考虑使用/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()

遗憾的是,这也不起作用.

问题

我知道我可以只复制相同的代码,但是还有没有办法改用这个库(因为它会更短更优雅)?如果是这样,我该怎么做?

解决方案

好的,我不知道如何使用这个库来安装 split-apk,但这里有一个简短的代码,它似乎可以使用不同的库:

build.gradle

//https://github.com/topjohnwu/libsu实现com.github.topjohnwu.libsu:core:2.5.1"

单个/拆分apk文件的基类:

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("需要一些方法来创建InputStream")}

获取root权限并安装:

<预><代码>Shell.getShell {val isRoot = it.isRootLog.d("AppLog", "isRoot ?$isRoot ")AsyncTask.execute {val apkFilesPath = "/storage/emulated/0/Download/split/"val fileInfoList = getFileInfoList(apkFilesPath)installSplitApkFiles(fileInfoList)}}

安装本身:

<预><代码>@工人线程私人乐趣 installSplitApkFiles(apkFiles: ArrayList): Boolean {如果(apkFiles.size == 1){//我们实际可以访问的单个文件,所以使用普通方法val apkFile = apkFiles[0]如果 (apkFiles[0].apkFile != null) {Log.d("AppLog", "安装单个 APK ${apkFile.name} ${apkFile.fileSize} ")val installResult = Shell.su("cat \"${apkFile.apkFile!!.absolutePath}\" | pm install -t -S ${apkFile.fileSize}").exec()Log.d("AppLog", "安装成功?${installResult.isSuccess}")如果(安装结果.isSuccess)返回真}}var sessionId: 整数?= 空Log.d("AppLog", "安装拆分的apk文件:$apkFiles")跑 {val sessionIdResult = Shell.su("pm install-create -r -t").exec().out//注意:可能需要使用这些来代替://"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 (apkFiles 中的 apkFile) {Log.d("AppLog", "安装 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 = Runtime.getRuntime().exec(command)val inputPipe = apkFile.getInputStream()尝试 {process.outputStream.use { outputStream ->inputPipe.copyTo(outputStream) }} catch (e: java.lang.Exception) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) process.destroyForcably() else process.destroy()抛出运行时异常(e)}进程.waitFor()val inputStr = process.inputStream.readBytes().toString(Charset.defaultCharset())val errStr = process.errorStream.readBytes().toString(Charset.defaultCharset())val isSucceeded = process.exitValue() == 0Log.d("AppLog", "isSucceeded?$isSucceeded inputStr:$inputStr errStr:$errStr")}//"pm install-commit %d ", sessionIdLog.d("AppLog", "提交...")val installResult = Shell.su("pm install-commit $sessionId").exec()Log.d("AppLog", "安装成功?${installResult.isSuccess}")返回 installResult.isSuccess}

以获取拆分的 apk 文件列表为例:

<预><代码>有趣的 SimpleDateFormat.tryParse(str: String) = try {解析(字符串)!= 空} catch (e: 异常) {错误的}@工人线程private fun getFileInfoList(splitApkPath: String): ArrayList{val parentFile = File(splitApkPath)val 结果 = ArrayList()if (parentFile.exists() && parentFile.canRead()) {val listFiles = parentFile.listFiles() ?: 返回 ArrayList()for(listFiles 中的文件)result.add(FileInfo(file.name, file.length(), file))返回结果}val longLines = Shell.su("ls -l $splitApkPath").exec().outval 模式 = Pattern.compile(" +")val formatter = SimpleDateFormat("HH:mm", Locale.getDefault())longLinesLoop@ for(longLines 中的行){//Log.d("AppLog", "line:$line")val 匹配器 = 模式匹配器(行)for (i 在 0 到 4)如果 (!matcher.find())继续@longLinesLoop//得到文件大小val startSizeStr = matcher.end()匹配器.find()val endSizeStr = matcher.start()val fileSizeStr = line.substring(startSizeStr, endSizeStr)而(真){val testTimeStr: 字符串 =line.substring(matcher.end(), line.indexOf(' ', matcher.end()))如果(格式化程序.tryParse(testTimeStr)){//找到时间,所以下一个是apkval 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)))休息}匹配器.find()}}//Log.d("AppLog", "result:${result.size}")返回结果}

Background

Using root, I know that for a single APK file, we can use the "libsu" library (here) to install as such:

val installResult = Shell.su("pm install -t \"$filePath\"").exec()

And if that failed (fails on new Android versions, not sure from which), as such (written about this here):

val installResult = Shell.su("cat \"$filePath\" | pm install -t -S ${apkSource.fileSize}").exec()

I also know that things got quite messy when it comes to installing split APK files (as shown here). First you need to create a session, using the "pm install-create" command:

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")
}

Then you have to "push" each of the APK files, as such:

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}")
}

And then you commit the changes using pm install-commit :

val installResult = Shell.su("pm install-commit $sessionId").exec()

Docs about it all:

  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.

The problem

This all worked fine till Android P, but for some reason, it failed on Q beta 6, showing me this error:

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/

What I've tried

This is similar to the case I've found for the single APK , here, so I thought that maybe a similar solution can be applied here too:

val result = Shell.su("cat $filePath | pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" \"$filePath\"").exec()

This still worked only on Android P and below.

So seeing that the original code I looked at worked, it uses an InputStream, which as the docs imply, is possible. Here's what they had:

while (apkSource.nextApk())
     ensureCommandSucceeded(Root.exec(String.format("pm install-write -S %d %d \"%s\"", apkSource.getApkLength(), sessionId, apkSource.getApkName()), apkSource.openApkInputStream()));

So what I tried is as such:

val result = Shell.su("pm install-write -S ${apkSource.fileSize} $sessionId \"${apkSource.fileName}\" -")
        .add(SuFileInputStream(filePath)).exec()

Sadly this also didn't work.

The question

I know I could just copy the same code, but is there still a way to use the library instead (because it will be shorter and more elegant)? If so, how could I do it?

解决方案

OK I don't know how to use this library to install the split-apk, but here's a short code that seems to work using a different library :

build.gradle

//https://github.com/topjohnwu/libsu
implementation "com.github.topjohnwu.libsu:core:2.5.1"

Base class of a single/split apk file :

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")
}

getting root and installing:


            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)
                }
            }

The installation itself:


    @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
    }

getting a list of split apk files as an example:


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
    }

这篇关于如何使用“libsu"库(或 adb)在 Android Q 上安装拆分的 APK 文件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆