当我只能处理文件或文件路径时如何处理 SAF?

How to handle SAF when I can only handle File or file-path?

背景

直到 Android Q,如果我们想获取有关 APK 文件的信息,我们可以在文件路径上使用 WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE to get access to the storage, and then use PackageManager.getPackageArchiveInfo 函数。

存在类似情况,例如在压缩文件上使用 ZipFile class,可能还有无数框架 API 和第三方库。

问题

Google最近对Android Q

宣布了大量限制

其中一个名为 Scoped Storage, which ruins storage permission when it comes to accessing all files the device has. It lets you either handle media files, or use the very restricted Storage-Access-Framework (SAF),它不允许应用使用文件 API 和文件路径访问和使用文件。

当 Android Q Beta 2 发布时,它破坏了很多应用程序,包括 Google。原因是它默认打开,影响所有应用程序,无论它们是否针对 Android Q。

原因是许多应用程序、SDK 和 Android 框架本身 - 都经常使用文件 API。在许多情况下,它们也不支持 InputStream 或 SAF 相关的解决方案。这方面的一个例子正是我写的 APK 解析示例 (PackageManager.getPackageArchiveInfo)。

但是,在 Q beta 3 上,情况发生了一些变化,因此以 Q 为目标的应用程序将拥有范围存储,并且有一个标志可以禁用它并仍然使用正常的存储权限和文件 API 照常。遗憾的是,该标志只是暂时的(阅读 here),因此它推迟了不可避免的事情。

我试过的

我已经尝试并发现了以下内容:

  1. 使用存储权限确实不允许我读取任何非媒体文件的文件(我想找到 APK 文件)。就好像文件不存在一样

  2. 使用 SAF,我可以找到 APK 文件,并通过一些解决方法找到它的真实路径(link here), I've noticed that File API can tell me that indeed the file exist, but it couldn't get its size, and the framework failed to use its path using getPackageArchiveInfo . Wrote about this here

  3. 我尝试对文件(linkhere)做一个symlink,然后从symlink中读取。没用。

  4. 对于解析APK文件的情况,我尝试搜索替代方案。我找到了 2 github 个使用文件 class (here and here), and one that uses InputStream instead ( here) 处理 APK 的存储库。遗憾的是,使用 InputStream 的那个非常旧,缺少各种功能(例如获取应用程序的名称和图标)并且不会很快更新。此外,拥有一个库需要维护以跟上Android的未来版本,否则将来可能会出现问题,甚至崩溃。

问题

  1. 一般来说,有没有办法在使用 SAF 时仍然使用 File API?我不是在谈论根解决方案或只是将文件复制到其他地方。我说的是更稳固的解决方案。

  2. 对于APK解析的情况,框架只提供file-path作为参数的问题,有没有办法解决?可能有任何解决方法或使用 InputStream 的方法吗?

当我只能处理文件或文件路径时,如何处理SAF?这是可能的,即使您只能将 Java 文件对象或路径字符串发送到您无法修改的库函数:

首先,获取一个你需要处理的文件的Uri(以String的形式就是"content://..."),然后:

    try {
        ParcelFileDescriptor parcelFileDescriptor =
                getContentResolver().openFileDescriptor(uri, "r"); // may get FileNotFoundException here
        // Obtain file descriptor:
        int fd = parcelFileDescriptor.getFd(); // or detachFd() if we want to close file in native code
        String linkFileName = "/proc/self/fd/" + fd;
        // Call library function with path/file string:
        someFunc(/*file name*/ linkFileName);
        // or with File parameter
        otherFunc(new File(linkFileName));
        // Finally, if you did not call detachFd() to obtain the file descriptor, call:
        parcelFileDescriptor.close();
        // Otherwise your library function should close file/stream...
    } catch (FileNotFoundException fnf) {
        fnf.printStackTrace(); // or whatever
    }          

发布另一个答案只是为了有更多空间并让我插入代码片段。鉴于我之前的回答中解释的文件描述符,我尝试使用 net.dongliu:apk-parser 包,@androiddeveloper 在原始问题中提到,如下(Lt.d 是我的 shorthand 使用Log.d(SOME_TAG, 字符串...)):

            String linkFileName = "/proc/self/fd/" + fd;
            try (ApkFile apkFile = new ApkFile(new File(linkFileName))) {
                ApkMeta apkMeta = apkFile.getApkMeta();
                Lt.d("ApkFile Label: ", apkMeta.getLabel());
                Lt.d("ApkFile pkg name: ", apkMeta.getPackageName());
                Lt.d("ApkFile version code: ", apkMeta.getVersionCode());
                String iconStr = apkMeta.getIcon();
                Lt.d("ApkFile icon str: ", iconStr);
                for (UseFeature feature : apkMeta.getUsesFeatures()) {
                    Lt.d(feature.getName());
                }
            }
            catch (Exception ex) {
                Lt.e("Exception in ApkFile code: ", ex);
                ex.printStackTrace();
            }
        }

它给了我正确的应用程序标签,对于图标,它只给了我一个资源目录的字符串(如 "res/drawable-mdpi-v4/fex.png"),因此必须再次应用原始 ZIP 读取功能来读取实际的图标位。具体来说,我正在测试 ES File Explorer Pro APK(购买了这个产品并保存了 APK 用于我自己的备份,得到以下输出:

 I/StorageTest: ApkFile Label: ES File Explorer Pro
 I/StorageTest: ApkFile pkg name: com.estrongs.android.pop.pro
 I/StorageTest: ApkFile version code: 1010
 I/StorageTest: ApkFile icon str: res/drawable-mdpi-v4/fex.png
 I/StorageTest: android.hardware.bluetooth
 I/StorageTest: android.hardware.touchscreen
 I/StorageTest: android.hardware.wifi
 I/StorageTest: android.software.leanback
 I/StorageTest: android.hardware.screen.portrait