Как использовать библиотеку libsu (или adb) для установки разделенных файлов APK на Android Q?

Фон

Используя root, я знаю, что для одного файла APK мы можем использовать библиотеку «libsu» (здесь ) для установки как таковой:

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? = 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, здесь, поэтому я подумал, что, возможно, подобное решение можно применить и здесь:

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

К сожалению, это тоже не сработало.

Вопрос

Я знаю, что могу просто скопировать тот же код, но есть ли способ использовать вместо него библиотеку (потому что она будет короче и элегантнее)? Если да, то как я мог это сделать?


person android developer    schedule 12.08.2019    source источник


Ответы (2)


Это грязно, но попробуйте этот код. Он использует 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 [ваш apk-файл] | pm install-write -S [size] [sessionId] [base/split apk name]», если вам не нужно устанавливать из потока. Если cat не работает, попробуйте вместо этого "dd if=[apk-файл]".

person user11227590    schedule 03.10.2019
comment
Кажется многообещающим. Не могли бы вы опубликовать его через Github? - person android developer; 10.10.2019
comment
@android-developer это всего лишь небольшой фрагмент из частного приложения. Не вижу смысла загружать его на GitHub. Не стесняйтесь использовать его и настраивать его, как вам нравится. - person user11227590; 10.10.2019

ОК, я не знаю, как использовать эту библиотеку для установки сплит-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")
}

получение рута и установка:


            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
    }
person android developer    schedule 22.02.2020