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

Posted

技术标签:

【中文标题】Android:从存储访问框架获得的 URI 中使用意图选择器打开文件【英文标题】:Android: Open file with intent chooser from URI obtained by Storage Access Framework 【发布时间】:2015-08-13 07:34:17 【问题描述】:

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

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

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

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

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

稍后,我想让用户打开这些文件(例如它们的名称显示在 UI 列表中,用户选择一个)。

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

谢谢,

【问题讨论】:

你试过类似new Intent(Intent.ACTION_VIEW, uri); 我尝试将视图意图用于存储访问框架的文件选择器返回的视频 URI。它会导致错误:“无法打开 fd for content://com.android.providers.media.documents/document/video:15026” 那行不通。您有权使用该Uri;其他应用无权使用该Uri 【参考方案1】:

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

原答案


我会远离现在的存储访问框架。它没有得到谷歌的充分支持,应用程序的支持也很糟糕,很难区分这些应用程序中的错误和 SAF 本身。如果你有足够的信心(这真的意味着“可以比一般的 Android 开发者更好地使用 try-catch 块”),自己使用存储访问框架,但只将旧的 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;
 

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

完整的解决方案


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

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

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

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; 

【讨论】:

另请注意,文件系统路径可能不可用,因为使用此代码的应用程序可能没有对指定位置的读取(更不用说写入)访问权限。 @CommonsWare 不是真的。我总是可以检测文件是否在外部存储上(或自己复制到那里)并检查目标应用程序是否有 READ_EXTERNAL_STORAGE 以确保它会处理我的file:// Uri,指向外部存储,就像任何其他。 content:// 没有这样的运气。 Android 系统内容提供者很幸运(他们通常将文件存储在可访问的位置并在_path 中提供),但自定义的内容很容易搞砸。并不是每个人都像 Google Drive 那样有影响力让第三方应用自爆。 大家好,我遇到了一个问题。我创建了一个像 github.com/googlesamples/android-StorageClient 这样的存储客户端和来自 github.com/googlesamples/android-StorageProvider 的客户端。我想在(word 应用程序或任何其他 3rd 方应用程序)中使用存储提供程序打开和 word 文档。请帮忙。 @Avneesh 我不能在这里给你任何建议,在 cmets 中。如果您需要有关实现客户端代码的建议或在 Stack Overflow 上写问题的帮助,欢迎您在聊天中讨论 — chat.***.com/rooms/146770/saf-misc 使用 /proc/self/fd/ 从 uri 获取外部 SD 文件名的代码也适用于 Android 10。谢谢!【参考方案2】:

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

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

他说:

这将从MediaProvider、DownloadsProvider、 和 ExternalStorageProvider,同时退回到非官方的 您提到的 ContentProvider 方法。

这些来自我的开源库aFileChooser。

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

【讨论】:

我已经看到了这个答案,但是让另一个应用程序处理用户选择的文件需要做很多工作。我正在使用存储访问提供程序提供的文件选择器来简化操作并避免使用文件选择器库。 此外,这是该方法的另一个问题:commonsware.com/blog/2014/07/04/uri-not-necessarily-file.html 好吧,我不推荐文件选择器库,那将是题外话。我的回答,或者实际上是 Paul Burke 的回答,是如何从各种 URI 中获取文件 URI。 是的,我很感激!我的观点是,这个练习使得使用 Storage Access Framework 的理由非常薄弱。如果获取实际文件路径是使 ACTION_VIEW 意图起作用的唯一解决方案,那么使用直接提供文件路径的选择器可能会更好。 FileUtils 对几个现有文件提供程序的文件路径和其他“解决方案”进行硬编码,并检查 _PATH 列,这至少可以说是不可靠的(另请参阅 this answer 以获取解释 why)。任何新的 Android 版本以及设备供应商的最轻微修改都可能破坏这些。安装自定义文件提供程序(例如替代文件管理器)是插件友好的存储访问框架结构的全部要点,它也会使那些“解决方案”失败。我的答案中的代码将总是可靠地确定路径。

以上是关于Android:从存储访问框架获得的 URI 中使用意图选择器打开文件的主要内容,如果未能解决你的问题,请参考以下文章

Android SAF(存储访问框架):从 TreeUri 获取特定文件 Uri

使用 KitKat 存储访问框架后打开 Google Drive File Content URI

存储访问框架获取正确的 Uri 路径删除/编辑/获取文件

Android:- 如何在整个应用程序中使动态 HashMap 可访问? [复制]

存储访问框架 - 权限被拒绝问题,即使授予权限

使用 SAF(存储访问框架)的 Android SD 卡写入权限