Android:使用存储访问框架获得的 URI 中的意图选择器打开文件

Android: Open file with intent chooser from URI obtained by Storage Access Framework

一开始,用户可以使用新的存储访问框架 select 文件(假设应用是 API>19):

https://developer.android.com/guide/topics/providers/document-provider.html

然后我通过保存如下所示的 URI 来保存对这些选定文件的引用:

content://com.android.providers.downloads.documments/document/745

(在这种情况下,文件来自默认下载目录`)。

稍后,我想让用户打开这些文件(例如他们的名字显示在 UI 列表中,而用户 select 是一个)。

我想用 Android 著名的意图选择器功能来做到这一点,而我只有上面的 URI 对象...

谢谢,

编辑: 我修改了这个答案以包含我最初称为 "writing a specialized ContentProvider" 的示例方法代码。这应该完全满足问题的要求。可能会使答案太大,但它现在具有内部代码依赖性,所以让我们把它作为一个整体。要点仍然成立:如果需要,请使用下面的 ContentPrvder,但请尝试将 file:// Uris 提供给支持它们的应用程序,除非您想因某人的应用程序崩溃而受到指责。

原回答


我会像现在这样远离存储访问框架。 Google 对它的支持不足,应用程序的支持也很糟糕,因此很难区分这些应用程序中的错误和 SAF 本身。如果您有足够的信心(这实际上意味着 "can use try-catch block better then average Android developer"),请自己使用存储访问框架,但仅将良好的 file:// 路径传递给其他人。

您可以使用以下技巧从 ParcelFileDescriptor 获取文件系统路径(您可以通过调用 openFileDescriptor 从 ContentResolver 获取它):

class FdCompat {
 public static String getFdPath(ParcelFileDescriptor fd) {
  final String resolved;

  try {
   final File procfsFdFile = new File("/proc/self/fd/" + fd.getFd());

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Returned name may be empty or "pipe:", "socket:", "(deleted)" etc.
    resolved = Os.readlink(procfsFdFile.getAbsolutePath());
   } else {
    // Returned name is usually valid or empty, but may start from
    // funny prefix if the file does not have a name
    resolved = procfsFdFile.getCanonicalPath();
   }

  if (TextUtils.isEmpty(resolved) || resolved.charAt(0) != '/'
                || resolved.startsWith("/proc/") || resolved.startsWith("/fd/"))
   return null;
  } catch (IOException ioe) {
   // This exception means, that given file DID have some name, but it is 
   // too long, some of symlinks in the path were broken or, most
   // likely, one of it's directories is inaccessible for reading.
   // Either way, it is almost certainly not a pipe.
   return "";
  } catch (Exception errnoe) {
   // Actually ErrnoException, but base type avoids VerifyError on old versions
   // This exception should be VERY rare and means, that the descriptor
   // was made unavailable by some Unix magic.
   return null;
  }

  return resolved;
 }
}

您必须做好准备,上面的方法将 return null(文件是管道或套接字,这是完全合法的)或空路径(没有对文件父目录的读取权限)。如果发生这种情况将整个流复制到您可以访问的某个目录

完整解决方案


如果您真的想坚持使用内容提供商 Uris,请继续。拿下面ContentProvider的代码。粘贴到您的应用中(并在 AndroidManifest 中注册)。使用下面的 getShareableUri 方法将收到的存储访问框架 URI 转换为您自己的。将该 Uri 传递给其他应用程序而不是原始 Uri。

下面的代码是不安全的(您可以很容易地使其安全,但解释一下会使这个答案的长度超出想象)。如果您关心,请使用 file:// Uris——Linux 文件系统被广泛认为足够安全。

扩展下面的解决方案以提供没有相应 Uri 的任意文件描述符留作 reader 的练习。

public class FdProvider extends ContentProvider {
 private static final String ORIGINAL_URI = "o";
 private static final String FD = "fd";
 private static final String PATH = "p";

 private static final Uri BASE_URI = 
     Uri.parse("content://com.example.fdhelper/");

 // Create an Uri from some other Uri and (optionally) corresponding
 // file descriptor (if you don't plan to close it until your process is dead).
 public static Uri getShareableUri(@Nullable ParcelFileDescriptor fd,
                                   Uri trueUri) {
     String path = fd == null ? null : FdCompat.getFdPath(fd);
     String uri = trueUri.toString();

     Uri.Builder builder = BASE_URI.buildUpon();

     if (!TextUtils.isEmpty(uri))
         builder.appendQueryParameter(ORIGINAL_URI, uri);

     if (fd != null && !TextUtils.isEmpty(path))
         builder.appendQueryParameter(FD, String.valueOf(fd.getFd()))
                .appendQueryParameter(PATH, path);

     return builder.build();
 }

 public boolean onCreate() { return true; }

 public ParcelFileDescriptor openFile(Uri uri, String mode)
     throws FileNotFoundException {

     String o = uri.getQueryParameter(ORIGINAL_URI);
     String fd = uri.getQueryParameter(FD);
     String path = uri.getQueryParameter(PATH);

     if (TextUtils.isEmpty(o)) return null;

     // offer the descriptor directly, if our process still has it
     try {
         if (!TextUtils.isEmpty(fd) && !TextUtils.isEmpty(path)) {
             int intFd = Integer.parseInt(fd);

             ParcelFileDescriptor desc = ParcelFileDescriptor.fromFd(intFd);

             if (intFd >= 0 && path.equals(FdCompat.getFdPath(desc))) {
                 return desc;
             }
         }
     } catch (RuntimeException | IOException ignore) {}

     // otherwise just forward the call
     try {
         Uri trueUri = Uri.parse(o);

         return getContext().getContentResolver()
             .openFileDescriptor(trueUri, mode);
     }
     catch (RuntimeException ignore) {}

     throw new FileNotFoundException();
 }

 // all other calls are forwarded the same way as above
 public Cursor query(Uri uri, String[] projection, String selection,
     String[] selectionArgs, String sortOrder) {

     String o = uri.getQueryParameter(ORIGINAL_URI);

     if (TextUtils.isEmpty(o)) return null;

     try {
         Uri trueUri = Uri.parse(o);

         return getContext().getContentResolver().query(trueUri, projection,
             selection, selectionArgs, sortOrder);
     } catch (RuntimeException ignore) {}

     return null;
 }

 public String getType(Uri uri) {
     String o = uri.getQueryParameter(ORIGINAL_URI);

     if (TextUtils.isEmpty(o)) return "*/*";

     try {
         Uri trueUri = Uri.parse(o);

         return getContext().getContentResolver().getType(trueUri);
     } catch (RuntimeException e) { return null; }
 }

 public Uri insert(Uri uri, ContentValues values) {
     return null;
 }

 public int delete(Uri uri, String selection, String[] selectionArgs) {
     return 0;
 }

 public int update(Uri uri, ContentValues values, String selection,
     String[] selectionArgs) { return 0; }
}

好的,SO上已经提供了解决方案,您只需搜索即可。

这是answer by Paul Burke。他编写了一个实用程序 class,其中 returns 此类内容路径的完整文件路径。

他说:

This will get the file path from the MediaProvider, DownloadsProvider, and ExternalStorageProvider, while falling back to the unofficial ContentProvider method you mention.

These are taken from my open source library, aFileChooser.

FileUtils.java 是 Paul Burke 写下您正在寻找的方法的地方。