Android:从存储访问框架获取的 URI 中使用意图选择器打开文件 [英] Android: Open file with intent chooser from URI obtained by Storage Access Framework

查看:22
本文介绍了Android:从存储访问框架获取的 URI 中使用意图选择器打开文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In the beginning the user can select files with the new Storage Access Framework (Assuming the app is API>19):

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

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

Then I save references to those chosen files by saving the URI`s which looks like:

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

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

(in this case the file is from the default downloads dir`).

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

Later, I want let the user to open those files (For example they names displayed in UI list, and the user selects one).

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

I want to do this with the Android famous intent chooser feature, and all I have is the above URI object...

谢谢,

推荐答案

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

I have revised this answer to include the example code of approach I have initially referred to as "writing a specialized ContentProvider". This should fully satisfy requirements of the question. Probably makes the answer too big, but it has internal code dependencies now, so let's leave it as whole. The main point still holds true: use the ContentPrvder below if you want, but try to give file:// Uris to apps, that support them, unless you want to be blamed for someone's app crashing.

原答案

我会远离现在的存储访问框架.它没有得到谷歌的充分支持,而且应用程序的支持非常糟糕,这使得很难区分这些应用程序中的错误和 SAF 本身.如果您有足够的信心(这真的意味着可以比普通的 Android 开发人员更好地使用 try-catch 块"),请自己使用存储访问框架,但仅将旧的 file:// 路径传递给其他人.

I would stay away from Storage Access Framework as it is now. It's insufficiently backed by Google, and the support in apps is abysmal, making it is hard to tell between bugs in those apps and SAF itself. If you are confident enough (which really means "can use try-catch block better then average Android developer"), use Storage Access Framework yourself, but pass to others only good-old file:// paths.

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

You can use the following trick to get filesystem path from ParcelFileDescriptor (you can get it from ContentResolver by calling openFileDescriptor):

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(文件是管道或套接字,这是完全合法的)或空路径(对文件的父目录没有读访问权限).如果发生这种情况将整个流复制到您可以访问的某个目录.

You must be prepared, that the method above will return null (the file is a pipe or socket, which is perfectly legitimate) or an empty path (no read access to file's parent directory). If this happens copy the entire stream to some directory you can access.

完整的解决方案

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

If you really want to stick with content provider Uris, here you go. Take the code of ContentProvider below. Paste into your app (and register it in AndroidManifest). Use getShareableUri method below to convert received Storage Access Framework Uri into your own. Pass that Uri to other apps instead of the original Uri.

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

The code below is insecure (you can easily make it secure, but explaining that would expand the length of this answer beyond imagination). If you care, use file:// Uris—Linux file systems are widely considered secure enough.

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

Extending the solution below to provide arbitrary file descriptors without corresponding Uri is left as exercise for 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; }
}

这篇关于Android:从存储访问框架获取的 URI 中使用意图选择器打开文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆