【问题标题】:How to use "libsu" library (or adb) to install split APK files on Android Q?如何使用“libsu”库(或 adb)在 Android Q 上安装拆分的 APK 文件?
【发布时间】:2019-08-12 21:10:36
【问题描述】:

背景

使用root,我知道对于单个APK文件,我们可以使用“libsu”库(here)这样安装:

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

如果失败(在新的 Android 版本上失败,不确定是从哪个版本开始的),那么(关于这个here):

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

然后您必须“推送”每个 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 here 找到的情况相似,所以我认为也许在这里也可以应用类似的解决方案:

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

很遗憾,这也没有用。

问题

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

【问题讨论】:

    标签: android adb android-install-apk android-app-bundle android-10.0


    【解决方案1】:

    这很乱,但试试这段代码。它使用 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 不起作用,请尝试“dd if=[apk file]”。

    【讨论】:

    • 看起来很有希望。可以通过 Github 发布吗?
    • @android-developer 这只是一个来自私有应用程序的小型 sn-p。它没有理由将其上传到 GitHub。随意使用它并随意调整它。
    【解决方案2】:

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

    build.gradle

    //https://github.com/topjohnwu/libsu
    implementation "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("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
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-11
      相关资源
      最近更新 更多