使用 root 安装 APK,处理“/data/local/tmp/”文件夹的新限制

Install APK using root, handling new limitations of "/data/local/tmp/" folder

背景

到目前为止,我能够通过以下代码使用 root(在应用程序中)安装 APK 文件:

pm install -t -f fullPathToApkFile

如果我想(尝试)安装到 SD 卡:

pm install -t -s fullPathToApkFile

问题

最近,不确定来自哪个 Android 版本(问题至少在 Android P beta 上存在),上述方法失败,显示此消息:

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/FDroid.apk, context u:r:system_server:s0)
Error: Unable to open file: /storage/emulated/0/Download/FDroid.apk
Consider using a file under /data/local/tmp/
Error: Can't open file: /storage/emulated/0/Download/FDroid.apk
Exception occurred while executing:
java.lang.IllegalArgumentException: Error: Can't open file: /storage/emulated/0/Download/FDroid.apk
    at com.android.server.pm.PackageManagerShellCommand.setParamsSize(PackageManagerShellCommand.java:306)
    at com.android.server.pm.PackageManagerShellCommand.runInstall(PackageManagerShellCommand.java:884)
    at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:138)
    at android.os.ShellCommand.exec(ShellCommand.java:103)
    at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:21125)
    at android.os.Binder.shellCommand(Binder.java:634)
    at android.os.Binder.onTransact(Binder.java:532)
    at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2806)
    at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:3841)
    at android.os.Binder.execTransact(Binder.java:731)

这似乎也影响了流行的应用程序,例如“Titanium backup (pro)”,无法恢复应用程序。

我试过的

查看所写的内容,它似乎缺少安装不在 /data/local/tmp/.

中的 APK 文件的权限

所以我尝试了接下来的事情,看看我是否能克服它:

  1. 设置对文件的访问权限 (chmod 777) - 没有帮助。
  2. 向我的应用程序授予存储和 REQUEST_INSTALL_PACKAGES (using ACTION_MANAGE_UNKNOWN_APP_SOURCES 意图的权限)- 没有帮助。
  3. 为文件创建一个symlink,这样它就会在/data/local/tmp/里面,使用官方的API:

     Os.symlink(fullPathToApkFile, symLinkFilePath)
    

    这没有做任何事情。

  4. 创建一个 symlink 使用这个 :

     ln -sf $fullPathToApkFile $symLinkFilePath
    

    这部分奏效了。该文件在那里,正如我在 Total Commander 应用程序中看到的那样,但是当我尝试检查它是否存在时,以及当我尝试从那里安装 APK 时,它失败了。

  5. Copy/move(使用 cpmv)文件到 /data/local/tmp/ 路径,然后从那里安装。这行得通,但也有缺点:移动是有风险的,因为它会暂时隐藏原始文件,并且会更改原始文件的时间戳。复制是不好的,因为使用额外的 space 只是为了安装(即使是暂时的),因为这样做会浪费时间。

  6. 复制 APK 文件,告诉它避免实际复制(意思是硬 link),使用此命令(取自 here):

     cp -p -r -l $fullPathToApkFile $tempFileParentPath"
    

    这没有用。它给我带来了这个错误:

     cp: /data/local/tmp/test.apk: Cross-device link
    
  7. 检查在安装应用程序的其他情况下会发生什么。当您通过 IDE 安装时,它实际上会在此特殊路径中创建 APK 文件,但如果您通过 Play 商店安装,简单的 APK 安装(通过 Intent)或 adb(通过 PC),它不会t.

  8. 也在这里写过:https://issuetracker.google.com/issues/80270303

问题

  1. 有什么方法可以克服在这个特殊路径上使用root安装APK的缺点吗?甚至可能完全避免处理这条路径?

  2. 为什么 OS 突然要求使用这个路径?为什么不像其他安装应用程序的方法那样使用原始路径呢?其他安装应用程序的方法有什么作用,以某种方式避免使用空间路径?

如果您不介意移动过程,一个解决方案是同时保存和恢复原始文件的时间戳,如下所示:

    val tempFileParentPath = "/data/local/tmp/"
    val tempFilePath = tempFileParentPath + File(fullPathToApkFile).name
    val apkTimestampTempFile = File(context.cacheDir, "apkTimestamp")
    apkTimestampTempFile.delete()
    apkTimestampTempFile.mkdirs()
    apkTimestampTempFile.createNewFile()
    root.runCommands("touch -r $fullPathToApkFile ${apkTimestampTempFile.absolutePath}")
    root.runCommands("mv $fullPathToApkFile $tempFileParentPath")
    root.runCommands("pm install -t -f $tempFilePath")
    root.runCommands("mv $tempFilePath $fullPathToApkFile")
    root.runCommands("touch -r ${apkTimestampTempFile.absolutePath} $fullPathToApkFile")
    apkTimestampTempFile.delete()

还是有点危险,不过比复制文件好...


编辑:Google 向我展示了一个很好的解决方法 (here) :

We don't support installation of APKs from random directories on the device. They either need to be installed directly from the host using 'adb install' or you have to stream the contents to install --

$ cat foo.apk | pm install -S APK_SIZE

虽然我认为他们不支持从随机路径安装 APK 文件是不正确的(以前一直有效),但解决方法似乎确实有效。我需要更改安装 APK 文件的代码如下:

val length = File(fullPathToApkFile ).length()
commands.add("cat $fullPathToApkFile | pm install -S $length")

事实是,现在我还有其他一些问题:

  1. 此解决方法是否可以避免将 APK moving/copying 存入存储,并且不影响原始文件? - 似乎是这样
  2. 这是否支持任何 APK 文件,甚至是大文件? - 对于一个需要 433MB 的 APK,它似乎成功地做到了,所以我认为它可以安全地用于所有大小。
  3. 只有Android P才需要,对吧? - 目前看来是这样。
  4. 为什么需要文件大小作为参数? - 不知道,但如果我删除它,它将不起作用

感谢您的回答!我也在其他地方寻找了一个完整的 OTA 设置,以便为 Android 10 等工作。它 100% 适用于 Samsung Galaxy Tab 10.1 运行 Android 10.

这是一篇带有代码的中篇文章: https://medium.com/@jnishu1996/over-the-air-ota-updates-for-android-apps-download-apk-silent-apk-installation-auto-launch-8ee6f342197c

神奇的是运行这个具有root权限的命令:

            process = Runtime.getRuntime().exec("su");
            out = process.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(out);
            // Get all file permissions
            dataOutputStream.writeBytes("chmod 777 " + file.getPath() + "\n");
            // Perform silent installation command, all flags are necessary for some reason, only this works reliably post Android 10
            String installCommand = "cat " + file.getAbsolutePath() + "| pm install -d -t -S " + file.length();
            // Data to send to the LaunchActivity to the app knows it got updated and performs necessary functions to notify backend
            // es stands for extraString
            // In LaunchActivity onCreate(), you can get this data by running -> if (getIntent().getStringExtra("OTA").equals("true"))
            String launchCommandIntentArguments = "--es OTA true --es messageId " + MyApplication.mLastSQSMessage.receiptHandle();
            // Start a background thread to wait for 8 seconds before reopening the app's LaunchActivity, and pass necessary arguments
            String launchCommand = "(sleep 8; am start -n co.getpresso.Presso/.activities.LaunchActivity " + launchCommandIntentArguments + ")&";

            // The entire command is deployed with a ";" in the middle to launchCommand run after installCommand
            String installAndLaunchCommand = installCommand + "; " + launchCommand;

            // begins the installation
            dataOutputStream.writeBytes(installAndLaunchCommand);
            dataOutputStream.flush();
            // Close the stream operation
            dataOutputStream.close();
            out.close();
            int value = process.waitFor();