Android - 使用 Google 范围存储 API 以本机 C/C++ 代码访问文件

Android - accessing files in native C/C++ code with Google Scoped Storage API

我需要在本机 C/C++ 代码中的 Android 应用程序中按文件名打开文件。本机代码是我不想修改的第 3 方库,但它们通常需要文件名作为 read/write 文件的参数。使用 Google 的 "scoped storage" API 并在 Android 10 或更高版本中禁用对文件的本机访问,这是一个真正的问题。

一个众所周知的解决方案是获取文件描述符并使用 "proc/self/fd/FD_NUMER" 技巧,例如:

       ParcelFileDescriptor mParcelFileDescriptor = null;

       String getFileNameThatICanUseInNativeCode(Context context, DocumentFile doc) { 
           try {
                Uri uri = doc.getUri();
                mParcelFileDescriptor =
                        context.getContentResolver().openFileDescriptor(uri, "r");
                if (mParcelFileDescriptor != null) {
                    int fd = mParcelFileDescriptor.getFd();
                    return "/proc/self/fd/" + fd;
                }
            }
            catch (FileNotFoundException fne) {
                return "";
            }
        }

        // Don't forget to close mParcelFileDescriptor when done!

将此传递给本机 C/C++ 代码是可行的,但 仅当文件位于 phone 主存储器中时 。如果用户试图打开插入 phone 插槽的外部 SD 卡上的文件,它不起作用 - 以这种方式打开的文件没有读取权限。我只能获取文件描述符 int 号并使用 fdopen(fd)。但这将需要修改 3rd 方库(开源或许可)的源代码,并且每当更新这些库的原始源代码时都会很头疼。

这个问题有更好的解决办法吗?不,我不想听到添加

的解决方案
android:requestLegacyExternalStorage="true"

到AndroidManifest.xml 应用程序部分 - Google 威胁要在 2020 年的 Android 的下一版本中禁用它,因此需要一个永久的解决方案。另一个简单但愚蠢的解决方案是将用户尝试打开的整个(可能很大)文件复制到私有应用程序目录中。又笨又没用...

2020 年 5 月 19 日更新:刚刚发现:当 Android 11 上的 运行 时,READ_EXTERNAL_STORAGE 权限允许您读取文件,但不能列出目录内容并定位 API 30(或将来更高)。我将其作为错误提交,并得到了 "This is working as intended" 回复(https://issuetracker.google.com/issues/156660903). If anyone cares, please comment there, and also at their "survey": https://google.qualtrics.com/jfe/form/SV_9HOzzyeCIEw0ij3?Source=scoped-storage 我不知道如何在 Android 上开发应用程序,但有所有这些限制。

2020 年 5 月 17 日更新:Google 终于承认并允许 READ_EXTERNAL_STORAGE 在 Android 11 及以后获得许可。在花了几个月的时间将我的应用程序转换为存储访问框架 (SAF) 之后,我现在需要一两周的时间将其转换回常规文件访问,至少是读取文件部分...谢谢,Google!讽刺地感谢您浪费时间和精力,并真诚地感谢您至少部分理解我们的观点!

所以,我之前在下面列出的答案将不再需要了,松一口气!

在 Android "Scoped Storage" B.S. 上又浪费了​​我生命中的美好一天后,我找到了一个可行的解决方案,无需修改第 3 方本机库的源代码,前提是拥有这些库的来源并可以构建它们。

我(非常不满意)的解决方案是:在C/C++编译命令中添加如下选项:

-include "[some/path/]idiocy_fopen_fd.h"

和 idiocy_fopen_fd.h 如下所示,如您所见,每次调用常规 fopen() 都被 idiocy_fopen_fd() 代码替换,该代码检查文件名是否以“ /proc/self/fd/”,如果是,则提取文件描述符编号并调用 fdopen() 而不是 fopen() ...如果有人有更好的解决方案,最好在您没有源代码时也能使用第三方库的代码,请分享。

#ifndef fopen_fd

#include <stdio.h>
#include <string.h>
#include <unistd.h> // for dup()

#ifdef __cplusplus
extern "C" {
#endif

inline FILE* idiocy_fopen_fd(const char* fname, const char * mode) {
  if (strstr(fname, "/proc/self/fd/") == fname) {
    int fd = atoi(fname + 14);
    if (fd != 0) {
      // Why dup(fd) below: if we called fdopen() on the
      // original fd value, and the native code closes
      // and tries re-open that file, the second fdopen(fd)
      // would fail, return NULL - after closing the
      // original fd received from Android, it's no longer valid.
      FILE *fp = fdopen(dup(fd), mode);
      // Why rewind(fp): if the native code closes and 
      // opens again the file, the file read/write position
      // would not change, because with dup(fd) it's still
      // the same file...
      rewind(fp);
      return fp;
    }
  }
  return fopen(fname, mode);
}
// Note that the above leaves the original file descriptor
// opened when finished - close parcelFileDescriptor in
// Java/Kotlin when your native code returns!

#ifdef __cplusplus
}
#endif

#define fopen idiocy_fopen_fd

#endif

嗨@gregko 我设法获得了真实路径而不是 fd 路径并将其发送到我的本地 activity 这稍微减轻了负担

  String getFileNameThatICanUseInNativeCode(Uri uri) throws FileNotFoundException {
      mParcelFileDescriptor =
              getApplicationContext().getContentResolver().openFileDescriptor(uri, "r");
      if (mParcelFileDescriptor != null) {
        int fd = mParcelFileDescriptor.getFd();
        File file = new File("/proc/self/fd/" + fd);
        String path = null;
        try {
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            path = Os.readlink(file.getAbsolutePath()).toString();
          }
        } catch (ErrnoException e) {
          e.printStackTrace();
        }

        return path;
      }
      else{
        return null;
      }
  }

使用这段代码我基本上做了一个完整的循环然后我得到 /storage/CC12-FF343/Games/game.zip 而不是 /proc/....