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