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

2

16

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

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`).

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

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

Thanks,

Jampack answered 30/5, 2015 at 12:53 Comment(3)
Have you tried something like new Intent(Intent.ACTION_VIEW, uri);Slow
I tried using the view intent for a video URI returned by the file picker from Storage Access Framework. It causes an error: "Couldn't open fd for content://com.android.providers.media.documents/document/video:15026"Urea
That is not going to work. You have rights to use that Uri; other apps do not have rights to use that Uri.Fatherhood
M
3

Edit: 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.

Original answer


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.

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;
 }
}

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.

Complete solution


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.

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.

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; }
}
Menadione answered 8/7, 2015 at 4:52 Comment(5)
Also note that the filesystem path may not be usable, as apps using this code may not have read (let alone write) access to the designated location.Fatherhood
@Fatherhood not really. I can always detect if the the file is on external storage (or copy it there myself) and check that target app has READ_EXTERNAL_STORAGE to be sure, that it will treat my file:// Uri, pointing to the external storage, as nicely as any other. No such luck with content://. Android system content providers are lucky (they generally store files in accessible locations and give it away in _path), but custom ones are easily screwed. Not everyone has as much influence as Google Drive to let third-party apps blow themselves up.Menadione
Hi all, i am facing an issue . i create a storage client like this github.com/googlesamples/android-StorageClient and client from github.com/googlesamples/android-StorageProvider . i want to open and word document using Storage provider in (word application or any other 3rd party application). please help.Crystacrystal
@Crystacrystal I can not suggest you anything here, in comments. If you want advice about implementing your client code or need help with writing a question on Stack Overflow, you are welcome to discuss that in chat — chat.stackoverflow.com/rooms/146770/saf-miscMenadione
Code for getting External SD file name from uri using /proc/self/fd/ also worked on Android 10. Thanks!Nice
G
1

Well the solution has already been provided on the SO and you only needed to search for it.

This is the answer by Paul Burke. He has written a utility class which returns complete file path for such a content path.

He says:

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

and

These are taken from my open source library, aFileChooser.

FileUtils.java is the place where Paul Burke has written the method you're looking for.

Grimaldo answered 8/7, 2015 at 6:39 Comment(6)
I have seen that answer but that's a lot of work to let another application handle a file selected by user. I was using the file picker provided by the Storage Access Provider to make things easier and avoid using file picker libraries.Urea
Besides, this is another issue with that approach: commonsware.com/blog/2014/07/04/uri-not-necessarily-file.htmlUrea
Well, I didn't recommend a file picker library, and that would be off-topic. My answer, or in fact Paul Burke's answer, is a how-to-get file URI from various URIs.Grimaldo
Yes, I appreciate that! My point is that this exercise makes the case for using Storage Access Framework very weak. It would probably be better to use a picker that directly provides a file path if getting the actual file path is the only solution to make the ACTION_VIEW intent work.Urea
FileUtils hardcode file paths and other "solutions" for several existing file providers and check for _PATH column, which is unreliable to say the least (see also this answer for explanation why). Any new Android version as well as slightest modification by device vendors can break those. Installing custom file providers (such as alternative file managers) is entire point of plugin-friendly Storage Access Framework structure, and it will make those "solutions" fail too. The code in my answer will always reliably determine the path.Menadione
@Menadione what you're posting is a way to share file with other apps which is completely different to what Paul Burke's utility class does. I don't know how you could compare the two.Grimaldo

© 2022 - 2024 — McMap. All rights reserved.