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/....
我需要在本机 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/....