Android - 使用Google Scoped Storage API在本地C/C++代码中访问文件

11
我需要在Android应用程序的本地C/C++代码中通过文件名打开文件。本地代码是第三方库,我希望不修改它们,但它们经常需要文件名作为读写文件的参数。使用Google的“范围存储”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++代码中可以正常工作,但仅当文件在手机主存储器中时。如果用户尝试打开插入到手机槽中的外部SD卡中的文件,则无法工作 - 以这种方式打开的文件没有读取权限。我只能获取文件描述符int编号并使用fdopen(fd)。但这将需要修改第三方库(开源或许可证)的源代码,并且每当这些库的原始源代码更新时都会带来很大的麻烦。

是否有更好的解决方法?不,我不想听添加

android:requestLegacyExternalStorage="true"

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


无法处理用户想要打开的文件路径不清楚。 - blackapps
这是外部SD卡上的任何路径,例如我在下载目录中获取了一个PDF文件的内容URI:content://com.android.externalstorage.documents/tree/BC4F-190B%3A/document/BC4F-190B%3ADownload%2FHprSnap6Man.pdf,实际文件路径为/mnt/media_rw/BC4F-190B/Download/HprSnap6Man.pdf,内容解析器给我fd 116,所以我尝试在本地代码中使用路径“/proc/self/fd/116”,但它无法读取。对于主存储上的文件,同样的技巧可以让我读取到可读文件。 - gregko
你尝试过这样做吗: ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - blackapps
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - 最终的代码与detachFd()或getFd()所做的相同,在我的问题中列出的代码没有区别。最终你需要一个整数fd号码发送给本地代码。 - gregko
2个回答

9

2020年5月19日更新:刚刚发现:在Android 11上运行并针对API 30(或将来更高版本)时,READ_EXTERNAL_STORAGE权限允许您读取文件,但不允许列出目录内容。我把它提交为一个错误,并收到了“这是预期的工作”回复(https://issuetracker.google.com/issues/156660903)。如果有人关心,请在那里评论,并在他们的“调查”中留言:https://google.qualtrics.com/jfe/form/SV_9HOzzyeCIEw0ij3?Source=scoped-storage 我不知道如何在所有这些限制下开发Android应用程序。

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

因此,我先前列出的答案不再需要,松了一口气!

在将另一个美好的一天浪费在Android“Scoped Storage”上之后,我找到了一个解决方案,可以在不修改第三方原生库源代码的情况下使用,前提是拥有这些库的源代码并且可以构建它们。

我的(非常不令人满意的)解决方案是:将以下选项添加到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

1
如果您的第三方库是静态的,您可以在链接脚本中将fopen重定向到idiocy_fopen_fd。如果它是共享的,可以通过操作ELF标头来实现重定向,例如参见https://www.codeproject.com/Articles/70302/Redirecting-functions-in-shared-ELF-libraries。 - Alex Cohn
@gregko 我有一个类似的情况,在经过大量测试后,我设法将你的代码破解成这个。 - Radius

0

嗨@gregko,我成功获取了真实路径而不是fd路径,并将其发送到我的本地活动中,这样减轻了一些负担。

  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/...。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接