1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.externalstorage;
18 
19 import android.annotation.Nullable;
20 import android.app.usage.StorageStatsManager;
21 import android.content.ContentResolver;
22 import android.content.UriPermission;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MatrixCursor.RowBuilder;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.Bundle;
29 import android.os.Environment;
30 import android.os.IBinder;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.os.storage.DiskInfo;
34 import android.os.storage.StorageManager;
35 import android.os.storage.VolumeInfo;
36 import android.provider.DocumentsContract;
37 import android.provider.DocumentsContract.Document;
38 import android.provider.DocumentsContract.Path;
39 import android.provider.DocumentsContract.Root;
40 import android.provider.MediaStore;
41 import android.provider.Settings;
42 import android.system.ErrnoException;
43 import android.system.Os;
44 import android.system.OsConstants;
45 import android.text.TextUtils;
46 import android.util.ArrayMap;
47 import android.util.DebugUtils;
48 import android.util.Log;
49 import android.util.Pair;
50 
51 import com.android.internal.annotations.GuardedBy;
52 import com.android.internal.content.FileSystemProvider;
53 import com.android.internal.util.IndentingPrintWriter;
54 
55 import java.io.File;
56 import java.io.FileDescriptor;
57 import java.io.FileNotFoundException;
58 import java.io.IOException;
59 import java.io.PrintWriter;
60 import java.util.Collections;
61 import java.util.List;
62 import java.util.Objects;
63 import java.util.UUID;
64 
65 public class ExternalStorageProvider extends FileSystemProvider {
66     private static final String TAG = "ExternalStorage";
67 
68     private static final boolean DEBUG = false;
69 
70     public static final String AUTHORITY = DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
71 
72     private static final Uri BASE_URI =
73             new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
74 
75     // docId format: root:path/to/file
76 
77     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
78             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
79             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_QUERY_ARGS
80     };
81 
82     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
83             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
84             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
85     };
86 
87     private static class RootInfo {
88         public String rootId;
89         public String volumeId;
90         public UUID storageUuid;
91         public int flags;
92         public String title;
93         public String docId;
94         public File visiblePath;
95         public File path;
96         public boolean reportAvailableBytes = true;
97     }
98 
99     private static final String ROOT_ID_PRIMARY_EMULATED =
100             DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID;
101     private static final String ROOT_ID_HOME = "home";
102 
103     private StorageManager mStorageManager;
104     private UserManager mUserManager;
105 
106     private final Object mRootsLock = new Object();
107 
108     @GuardedBy("mRootsLock")
109     private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
110 
111     @Override
onCreate()112     public boolean onCreate() {
113         super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
114 
115         mStorageManager = getContext().getSystemService(StorageManager.class);
116         mUserManager = getContext().getSystemService(UserManager.class);
117 
118         updateVolumes();
119         return true;
120     }
121 
enforceShellRestrictions()122     private void enforceShellRestrictions() {
123         if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
124                 && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
125             throw new SecurityException(
126                     "Shell user cannot access files for user " + UserHandle.myUserId());
127         }
128     }
129 
130     @Override
enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)131     protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
132             throws SecurityException {
133         enforceShellRestrictions();
134         return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
135     }
136 
137     @Override
enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)138     protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
139             throws SecurityException {
140         enforceShellRestrictions();
141         return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
142     }
143 
updateVolumes()144     public void updateVolumes() {
145         synchronized (mRootsLock) {
146             updateVolumesLocked();
147         }
148     }
149 
150     @GuardedBy("mRootsLock")
updateVolumesLocked()151     private void updateVolumesLocked() {
152         mRoots.clear();
153 
154         VolumeInfo primaryVolume = null;
155         final int userId = UserHandle.myUserId();
156         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
157         for (VolumeInfo volume : volumes) {
158             if (!volume.isMountedReadable()) continue;
159 
160             final String rootId;
161             final String title;
162             final UUID storageUuid;
163             if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
164                 // We currently only support a single emulated volume mounted at
165                 // a time, and it's always considered the primary
166                 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume);
167                 rootId = ROOT_ID_PRIMARY_EMULATED;
168 
169                 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
170                     // This is basically the user's primary device storage.
171                     // Use device name for the volume since this is likely same thing
172                     // the user sees when they mount their phone on another device.
173                     String deviceName = Settings.Global.getString(
174                             getContext().getContentResolver(), Settings.Global.DEVICE_NAME);
175 
176                     // Device name should always be set. In case it isn't, though,
177                     // fall back to a localized "Internal Storage" string.
178                     title = !TextUtils.isEmpty(deviceName)
179                             ? deviceName
180                             : getContext().getString(R.string.root_internal_storage);
181                     storageUuid = StorageManager.UUID_DEFAULT;
182                 } else {
183                     // This should cover all other storage devices, like an SD card
184                     // or USB OTG drive plugged in. Using getBestVolumeDescription()
185                     // will give us a nice string like "Samsung SD card" or "SanDisk USB drive"
186                     final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
187                     title = mStorageManager.getBestVolumeDescription(privateVol);
188                     storageUuid = StorageManager.convert(privateVol.fsUuid);
189                 }
190             } else if ((volume.getType() == VolumeInfo.TYPE_PUBLIC
191                             || volume.getType() == VolumeInfo.TYPE_STUB)
192                     && volume.getMountUserId() == userId) {
193                 rootId = volume.getFsUuid();
194                 title = mStorageManager.getBestVolumeDescription(volume);
195                 storageUuid = null;
196             } else {
197                 // Unsupported volume; ignore
198                 continue;
199             }
200 
201             if (TextUtils.isEmpty(rootId)) {
202                 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
203                 continue;
204             }
205             if (mRoots.containsKey(rootId)) {
206                 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
207                 continue;
208             }
209 
210             final RootInfo root = new RootInfo();
211             mRoots.put(rootId, root);
212 
213             root.rootId = rootId;
214             root.volumeId = volume.id;
215             root.storageUuid = storageUuid;
216             root.flags = Root.FLAG_LOCAL_ONLY
217                     | Root.FLAG_SUPPORTS_SEARCH
218                     | Root.FLAG_SUPPORTS_IS_CHILD;
219 
220             final DiskInfo disk = volume.getDisk();
221             if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
222             if (disk != null && disk.isSd()) {
223                 root.flags |= Root.FLAG_REMOVABLE_SD;
224             } else if (disk != null && disk.isUsb()) {
225                 root.flags |= Root.FLAG_REMOVABLE_USB;
226             }
227 
228             if (volume.getType() != VolumeInfo.TYPE_EMULATED) {
229                 root.flags |= Root.FLAG_SUPPORTS_EJECT;
230             }
231 
232             if (volume.isPrimary()) {
233                 // save off the primary volume for subsequent "Home" dir initialization.
234                 primaryVolume = volume;
235                 root.flags |= Root.FLAG_ADVANCED;
236             }
237             // Dunno when this would NOT be the case, but never hurts to be correct.
238             if (volume.isMountedWritable()) {
239                 root.flags |= Root.FLAG_SUPPORTS_CREATE;
240             }
241             root.title = title;
242             if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
243                 root.flags |= Root.FLAG_HAS_SETTINGS;
244             }
245             if (volume.isVisibleForRead(userId)) {
246                 root.visiblePath = volume.getPathForUser(userId);
247             } else {
248                 root.visiblePath = null;
249             }
250             root.path = volume.getInternalPathForUser(userId);
251             try {
252                 root.docId = getDocIdForFile(root.path);
253             } catch (FileNotFoundException e) {
254                 throw new IllegalStateException(e);
255             }
256         }
257 
258         // Finally, if primary storage is available we add the "Documents" directory.
259         // If I recall correctly the actual directory is created on demand
260         // by calling either getPathForUser, or getInternalPathForUser.
261         if (primaryVolume != null && primaryVolume.isVisible()) {
262             final RootInfo root = new RootInfo();
263             root.rootId = ROOT_ID_HOME;
264             mRoots.put(root.rootId, root);
265             root.title = getContext().getString(R.string.root_documents);
266 
267             // Only report bytes on *volumes*...as a matter of policy.
268             root.reportAvailableBytes = false;
269             root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH
270                     | Root.FLAG_SUPPORTS_IS_CHILD;
271 
272             // Dunno when this would NOT be the case, but never hurts to be correct.
273             if (primaryVolume.isMountedWritable()) {
274                 root.flags |= Root.FLAG_SUPPORTS_CREATE;
275             }
276 
277             // Create the "Documents" directory on disk (don't use the localized title).
278             root.visiblePath = new File(
279                     primaryVolume.getPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
280             root.path = new File(
281                     primaryVolume.getInternalPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
282             try {
283                 root.docId = getDocIdForFile(root.path);
284             } catch (FileNotFoundException e) {
285                 throw new IllegalStateException(e);
286             }
287         }
288 
289         Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
290 
291         // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
292         // as well as content://com.android.externalstorage.documents/document/*/children,
293         // so just notify on content://com.android.externalstorage.documents/.
294         getContext().getContentResolver().notifyChange(BASE_URI, null, false);
295     }
296 
resolveRootProjection(String[] projection)297     private static String[] resolveRootProjection(String[] projection) {
298         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
299     }
300 
301     @Override
getDocIdForFile(File file)302     protected String getDocIdForFile(File file) throws FileNotFoundException {
303         return getDocIdForFileMaybeCreate(file, false);
304     }
305 
getDocIdForFileMaybeCreate(File file, boolean createNewDir)306     private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
307             throws FileNotFoundException {
308         String path = file.getAbsolutePath();
309 
310         // Find the most-specific root path
311         boolean visiblePath = false;
312         RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
313 
314         if (mostSpecificRoot == null) {
315             // Try visible path if no internal path matches. MediaStore uses visible paths.
316             visiblePath = true;
317             mostSpecificRoot = getMostSpecificRootForPath(path, true);
318         }
319 
320         if (mostSpecificRoot == null) {
321             throw new FileNotFoundException("Failed to find root that contains " + path);
322         }
323 
324         // Start at first char of path under root
325         final String rootPath = visiblePath
326                 ? mostSpecificRoot.visiblePath.getAbsolutePath()
327                 : mostSpecificRoot.path.getAbsolutePath();
328         if (rootPath.equals(path)) {
329             path = "";
330         } else if (rootPath.endsWith("/")) {
331             path = path.substring(rootPath.length());
332         } else {
333             path = path.substring(rootPath.length() + 1);
334         }
335 
336         if (!file.exists() && createNewDir) {
337             Log.i(TAG, "Creating new directory " + file);
338             if (!file.mkdir()) {
339                 Log.e(TAG, "Could not create directory " + file);
340             }
341         }
342 
343         return mostSpecificRoot.rootId + ':' + path;
344     }
345 
getMostSpecificRootForPath(String path, boolean visible)346     private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
347         // Find the most-specific root path
348         RootInfo mostSpecificRoot = null;
349         String mostSpecificPath = null;
350         synchronized (mRootsLock) {
351             for (int i = 0; i < mRoots.size(); i++) {
352                 final RootInfo root = mRoots.valueAt(i);
353                 final File rootFile = visible ? root.visiblePath : root.path;
354                 if (rootFile != null) {
355                     final String rootPath = rootFile.getAbsolutePath();
356                     if (path.startsWith(rootPath) && (mostSpecificPath == null
357                             || rootPath.length() > mostSpecificPath.length())) {
358                         mostSpecificRoot = root;
359                         mostSpecificPath = rootPath;
360                     }
361                 }
362             }
363         }
364 
365         return mostSpecificRoot;
366     }
367 
368     @Override
getFileForDocId(String docId, boolean visible)369     protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
370         return getFileForDocId(docId, visible, true);
371     }
372 
getFileForDocId(String docId, boolean visible, boolean mustExist)373     private File getFileForDocId(String docId, boolean visible, boolean mustExist)
374             throws FileNotFoundException {
375         RootInfo root = getRootFromDocId(docId);
376         return buildFile(root, docId, visible, mustExist);
377     }
378 
resolveDocId(String docId, boolean visible)379     private Pair<RootInfo, File> resolveDocId(String docId, boolean visible)
380             throws FileNotFoundException {
381         RootInfo root = getRootFromDocId(docId);
382         return Pair.create(root, buildFile(root, docId, visible, true));
383     }
384 
getRootFromDocId(String docId)385     private RootInfo getRootFromDocId(String docId) throws FileNotFoundException {
386         final int splitIndex = docId.indexOf(':', 1);
387         final String tag = docId.substring(0, splitIndex);
388 
389         RootInfo root;
390         synchronized (mRootsLock) {
391             root = mRoots.get(tag);
392         }
393         if (root == null) {
394             throw new FileNotFoundException("No root for " + tag);
395         }
396 
397         return root;
398     }
399 
buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)400     private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)
401             throws FileNotFoundException {
402         final int splitIndex = docId.indexOf(':', 1);
403         final String path = docId.substring(splitIndex + 1);
404 
405         File target = visible ? root.visiblePath : root.path;
406         if (target == null) {
407             return null;
408         }
409         if (!target.exists()) {
410             target.mkdirs();
411         }
412         target = new File(target, path);
413         if (mustExist && !target.exists()) {
414             throw new FileNotFoundException("Missing file for " + docId + " at " + target);
415         }
416         return target;
417     }
418 
419     @Override
buildNotificationUri(String docId)420     protected Uri buildNotificationUri(String docId) {
421         return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
422     }
423 
424     @Override
onDocIdChanged(String docId)425     protected void onDocIdChanged(String docId) {
426         try {
427             // Touch the visible path to ensure that any sdcardfs caches have
428             // been updated to reflect underlying changes on disk.
429             final File visiblePath = getFileForDocId(docId, true, false);
430             if (visiblePath != null) {
431                 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK);
432             }
433         } catch (FileNotFoundException | ErrnoException ignored) {
434         }
435     }
436 
437     @Override
queryRoots(String[] projection)438     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
439         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
440         synchronized (mRootsLock) {
441             for (RootInfo root : mRoots.values()) {
442                 final RowBuilder row = result.newRow();
443                 row.add(Root.COLUMN_ROOT_ID, root.rootId);
444                 row.add(Root.COLUMN_FLAGS, root.flags);
445                 row.add(Root.COLUMN_TITLE, root.title);
446                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
447                 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
448 
449                 long availableBytes = -1;
450                 if (root.reportAvailableBytes) {
451                     if (root.storageUuid != null) {
452                         try {
453                             availableBytes = getContext()
454                                     .getSystemService(StorageStatsManager.class)
455                                     .getFreeBytes(root.storageUuid);
456                         } catch (IOException e) {
457                             Log.w(TAG, e);
458                         }
459                     } else {
460                         availableBytes = root.path.getUsableSpace();
461                     }
462                 }
463                 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
464             }
465         }
466         return result;
467     }
468 
469     @Override
findDocumentPath(@ullable String parentDocId, String childDocId)470     public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
471             throws FileNotFoundException {
472         final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false);
473         final RootInfo root = resolvedDocId.first;
474         File child = resolvedDocId.second;
475 
476         final File parent = TextUtils.isEmpty(parentDocId)
477                         ? root.path
478                         : getFileForDocId(parentDocId);
479 
480         return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child));
481     }
482 
getDocumentUri(String path, List<UriPermission> accessUriPermissions)483     private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions)
484             throws FileNotFoundException {
485         File doc = new File(path);
486 
487         final String docId = getDocIdForFile(doc);
488 
489         UriPermission docUriPermission = null;
490         UriPermission treeUriPermission = null;
491         for (UriPermission uriPermission : accessUriPermissions) {
492             final Uri uri = uriPermission.getUri();
493             if (AUTHORITY.equals(uri.getAuthority())) {
494                 boolean matchesRequestedDoc = false;
495                 if (DocumentsContract.isTreeUri(uri)) {
496                     final String parentDocId = DocumentsContract.getTreeDocumentId(uri);
497                     if (isChildDocument(parentDocId, docId)) {
498                         treeUriPermission = uriPermission;
499                         matchesRequestedDoc = true;
500                     }
501                 } else {
502                     final String candidateDocId = DocumentsContract.getDocumentId(uri);
503                     if (Objects.equals(docId, candidateDocId)) {
504                         docUriPermission = uriPermission;
505                         matchesRequestedDoc = true;
506                     }
507                 }
508 
509                 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) {
510                     // This URI permission provides everything an app can get, no need to
511                     // further check any other granted URI.
512                     break;
513                 }
514             }
515         }
516 
517         // Full permission URI first.
518         if (allowsBothReadAndWrite(treeUriPermission)) {
519             return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
520         }
521 
522         if (allowsBothReadAndWrite(docUriPermission)) {
523             return docUriPermission.getUri();
524         }
525 
526         // Then partial permission URI.
527         if (treeUriPermission != null) {
528             return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
529         }
530 
531         if (docUriPermission != null) {
532             return docUriPermission.getUri();
533         }
534 
535         throw new SecurityException("The app is not given any access to the document under path " +
536                 path + " with permissions granted in " + accessUriPermissions);
537     }
538 
allowsBothReadAndWrite(UriPermission permission)539     private static boolean allowsBothReadAndWrite(UriPermission permission) {
540         return permission != null
541                 && permission.isReadPermission()
542                 && permission.isWritePermission();
543     }
544 
545     @Override
querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)546     public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
547             throws FileNotFoundException {
548         final File parent;
549         synchronized (mRootsLock) {
550             parent = mRoots.get(rootId).path;
551         }
552 
553         return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs);
554     }
555 
556     @Override
ejectRoot(String rootId)557     public void ejectRoot(String rootId) {
558         final long token = Binder.clearCallingIdentity();
559         RootInfo root = mRoots.get(rootId);
560         if (root != null) {
561             try {
562                 mStorageManager.unmount(root.volumeId);
563             } catch (RuntimeException e) {
564                 throw new IllegalStateException(e);
565             } finally {
566                 Binder.restoreCallingIdentity(token);
567             }
568         }
569     }
570 
571     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)572     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
573         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
574         synchronized (mRootsLock) {
575             for (int i = 0; i < mRoots.size(); i++) {
576                 final RootInfo root = mRoots.valueAt(i);
577                 pw.println("Root{" + root.rootId + "}:");
578                 pw.increaseIndent();
579                 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
580                 pw.println();
581                 pw.printPair("title", root.title);
582                 pw.printPair("docId", root.docId);
583                 pw.println();
584                 pw.printPair("path", root.path);
585                 pw.printPair("visiblePath", root.visiblePath);
586                 pw.decreaseIndent();
587                 pw.println();
588             }
589         }
590     }
591 
592     @Override
call(String method, String arg, Bundle extras)593     public Bundle call(String method, String arg, Bundle extras) {
594         Bundle bundle = super.call(method, arg, extras);
595         if (bundle == null && !TextUtils.isEmpty(method)) {
596             switch (method) {
597                 case "getDocIdForFileCreateNewDir": {
598                     getContext().enforceCallingPermission(
599                             android.Manifest.permission.MANAGE_DOCUMENTS, null);
600                     if (TextUtils.isEmpty(arg)) {
601                         return null;
602                     }
603                     try {
604                         final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
605                         bundle = new Bundle();
606                         bundle.putString("DOC_ID", docId);
607                     } catch (FileNotFoundException e) {
608                         Log.w(TAG, "file '" + arg + "' not found");
609                         return null;
610                     }
611                     break;
612                 }
613                 case MediaStore.GET_DOCUMENT_URI_CALL: {
614                     // All callers must go through MediaProvider
615                     getContext().enforceCallingPermission(
616                             android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
617 
618                     final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
619                     final List<UriPermission> accessUriPermissions = extras
620                             .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS);
621 
622                     final String path = fileUri.getPath();
623                     try {
624                         final Bundle out = new Bundle();
625                         final Uri uri = getDocumentUri(path, accessUriPermissions);
626                         out.putParcelable(DocumentsContract.EXTRA_URI, uri);
627                         return out;
628                     } catch (FileNotFoundException e) {
629                         throw new IllegalStateException("File in " + path + " is not found.", e);
630                     }
631                 }
632                 case MediaStore.GET_MEDIA_URI_CALL: {
633                     // All callers must go through MediaProvider
634                     getContext().enforceCallingPermission(
635                             android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
636 
637                     final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
638                     final String docId = DocumentsContract.getDocumentId(documentUri);
639                     try {
640                         final Bundle out = new Bundle();
641                         final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
642                         out.putParcelable(DocumentsContract.EXTRA_URI, uri);
643                         return out;
644                     } catch (FileNotFoundException e) {
645                         throw new IllegalStateException(e);
646                     }
647                 }
648                 default:
649                     Log.w(TAG, "unknown method passed to call(): " + method);
650             }
651         }
652         return bundle;
653     }
654 }
655