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 static java.util.regex.Pattern.CASE_INSENSITIVE;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.usage.StorageStatsManager;
24 import android.content.AttributionSource;
25 import android.content.ContentResolver;
26 import android.content.UriPermission;
27 import android.database.Cursor;
28 import android.database.MatrixCursor;
29 import android.database.MatrixCursor.RowBuilder;
30 import android.net.Uri;
31 import android.os.Binder;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.os.UserHandle;
35 import android.os.UserManager;
36 import android.os.storage.DiskInfo;
37 import android.os.storage.StorageEventListener;
38 import android.os.storage.StorageManager;
39 import android.os.storage.VolumeInfo;
40 import android.provider.DocumentsContract;
41 import android.provider.DocumentsContract.Document;
42 import android.provider.DocumentsContract.Path;
43 import android.provider.DocumentsContract.Root;
44 import android.provider.Settings;
45 import android.system.ErrnoException;
46 import android.system.Os;
47 import android.system.OsConstants;
48 import android.text.TextUtils;
49 import android.util.ArrayMap;
50 import android.util.DebugUtils;
51 import android.util.Log;
52 import android.util.Pair;
53 
54 import com.android.internal.annotations.GuardedBy;
55 import com.android.internal.annotations.VisibleForTesting;
56 import com.android.internal.content.FileSystemProvider;
57 import com.android.internal.util.IndentingPrintWriter;
58 
59 import java.io.File;
60 import java.io.FileDescriptor;
61 import java.io.FileNotFoundException;
62 import java.io.IOException;
63 import java.io.PrintWriter;
64 import java.util.Collections;
65 import java.util.List;
66 import java.util.Locale;
67 import java.util.Objects;
68 import java.util.UUID;
69 import java.util.regex.Pattern;
70 
71 /**
72  * Presents content of the shared (a.k.a. "external") storage.
73  * <p>
74  * Starting with Android 11 (R), restricts access to the certain sections of the shared storage:
75  * {@code Android/data/}, {@code Android/obb/} and {@code Android/sandbox/}, that will be hidden in
76  * the DocumentsUI by default.
77  * See <a href="https://developer.android.com/about/versions/11/privacy/storage">
78  * Storage updates in Android 11</a>.
79  * <p>
80  * Documents ID format: {@code root:path/to/file}.
81  */
82 public class ExternalStorageProvider extends FileSystemProvider {
83     private static final String TAG = "ExternalStorage";
84 
85     private static final boolean DEBUG = false;
86 
87     public static final String AUTHORITY = DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
88 
89     private static final Uri BASE_URI =
90             new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
91 
92     /**
93      * Regex for detecting {@code /Android/data/}, {@code /Android/obb/} and
94      * {@code /Android/sandbox/} along with all their subdirectories and content.
95      */
96     private static final Pattern PATTERN_RESTRICTED_ANDROID_SUBTREES =
97             Pattern.compile("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE);
98 
99     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
100             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
101             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_QUERY_ARGS
102     };
103 
104     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
105             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
106             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
107     };
108 
109     private static class RootInfo {
110         public String rootId;
111         public String volumeId;
112         public UUID storageUuid;
113         public int flags;
114         public String title;
115         public String docId;
116         public File visiblePath;
117         public File path;
118         // TODO (b/157033915): Make getFreeBytes() faster
119         public boolean reportAvailableBytes = false;
120     }
121 
122     private static final String ROOT_ID_PRIMARY_EMULATED =
123             DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID;
124 
125     private static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
126     private static final String GET_MEDIA_URI_CALL = "get_media_uri";
127 
128     private StorageManager mStorageManager;
129     private UserManager mUserManager;
130 
131     private final Object mRootsLock = new Object();
132 
133     @GuardedBy("mRootsLock")
134     private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
135 
136     @Override
onCreate()137     public boolean onCreate() {
138         super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
139 
140         mStorageManager = getContext().getSystemService(StorageManager.class);
141         mUserManager = getContext().getSystemService(UserManager.class);
142 
143         updateVolumes();
144 
145         mStorageManager.registerListener(new StorageEventListener() {
146                 @Override
147                 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
148                     updateVolumes();
149                 }
150             });
151 
152         return true;
153     }
154 
enforceShellRestrictions()155     private void enforceShellRestrictions() {
156         if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
157                 && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
158             throw new SecurityException(
159                     "Shell user cannot access files for user " + UserHandle.myUserId());
160         }
161     }
162 
163     @Override
enforceReadPermissionInner(Uri uri, @NonNull AttributionSource attributionSource)164     protected int enforceReadPermissionInner(Uri uri,
165             @NonNull AttributionSource attributionSource) throws SecurityException {
166         enforceShellRestrictions();
167         return super.enforceReadPermissionInner(uri, attributionSource);
168     }
169 
170     @Override
enforceWritePermissionInner(Uri uri, @NonNull AttributionSource attributionSource)171     protected int enforceWritePermissionInner(Uri uri,
172             @NonNull AttributionSource attributionSource) throws SecurityException {
173         enforceShellRestrictions();
174         return super.enforceWritePermissionInner(uri, attributionSource);
175     }
176 
updateVolumes()177     public void updateVolumes() {
178         synchronized (mRootsLock) {
179             updateVolumesLocked();
180         }
181     }
182 
183     @GuardedBy("mRootsLock")
updateVolumesLocked()184     private void updateVolumesLocked() {
185         mRoots.clear();
186 
187         final int userId = UserHandle.myUserId();
188         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
189         for (VolumeInfo volume : volumes) {
190             if (!volume.isMountedReadable() || volume.getMountUserId() != userId) continue;
191 
192             final String rootId;
193             final String title;
194             final UUID storageUuid;
195             if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
196                 // We currently only support a single emulated volume per user mounted at
197                 // a time, and it's always considered the primary
198                 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume);
199                 rootId = ROOT_ID_PRIMARY_EMULATED;
200 
201                 if (volume.isPrimaryEmulatedForUser(userId)) {
202                     // This is basically the user's primary device storage.
203                     // Use device name for the volume since this is likely same thing
204                     // the user sees when they mount their phone on another device.
205                     String deviceName = Settings.Global.getString(
206                             getContext().getContentResolver(), Settings.Global.DEVICE_NAME);
207 
208                     // Device name should always be set. In case it isn't, though,
209                     // fall back to a localized "Internal Storage" string.
210                     title = !TextUtils.isEmpty(deviceName)
211                             ? deviceName
212                             : getContext().getString(R.string.root_internal_storage);
213                     storageUuid = StorageManager.UUID_DEFAULT;
214                 } else {
215                     // This should cover all other storage devices, like an SD card
216                     // or USB OTG drive plugged in. Using getBestVolumeDescription()
217                     // will give us a nice string like "Samsung SD card" or "SanDisk USB drive"
218                     final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
219                     title = mStorageManager.getBestVolumeDescription(privateVol);
220                     storageUuid = StorageManager.convert(privateVol.fsUuid);
221                 }
222             } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC
223                     || volume.getType() == VolumeInfo.TYPE_STUB) {
224                 rootId = volume.getFsUuid();
225                 title = mStorageManager.getBestVolumeDescription(volume);
226                 storageUuid = null;
227             } else {
228                 // Unsupported volume; ignore
229                 continue;
230             }
231 
232             if (TextUtils.isEmpty(rootId)) {
233                 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
234                 continue;
235             }
236             if (mRoots.containsKey(rootId)) {
237                 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
238                 continue;
239             }
240 
241             final RootInfo root = new RootInfo();
242             mRoots.put(rootId, root);
243 
244             root.rootId = rootId;
245             root.volumeId = volume.id;
246             root.storageUuid = storageUuid;
247             root.flags = Root.FLAG_LOCAL_ONLY
248                     | Root.FLAG_SUPPORTS_SEARCH
249                     | Root.FLAG_SUPPORTS_IS_CHILD;
250 
251             final DiskInfo disk = volume.getDisk();
252             if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
253             if (disk != null && disk.isSd()) {
254                 root.flags |= Root.FLAG_REMOVABLE_SD;
255             } else if (disk != null && disk.isUsb()) {
256                 root.flags |= Root.FLAG_REMOVABLE_USB;
257             }
258 
259             if (volume.getType() != VolumeInfo.TYPE_EMULATED
260                     && volume.getType() != VolumeInfo.TYPE_STUB) {
261                 root.flags |= Root.FLAG_SUPPORTS_EJECT;
262             }
263 
264             if (volume.isPrimary()) {
265                 root.flags |= Root.FLAG_ADVANCED;
266             }
267             // Dunno when this would NOT be the case, but never hurts to be correct.
268             if (volume.isMountedWritable()) {
269                 root.flags |= Root.FLAG_SUPPORTS_CREATE;
270             }
271             root.title = title;
272             if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
273                 root.flags |= Root.FLAG_HAS_SETTINGS;
274             }
275             if (volume.isVisibleForUser(userId)) {
276                 root.visiblePath = volume.getPathForUser(userId);
277             } else {
278                 root.visiblePath = null;
279             }
280             root.path = volume.getInternalPathForUser(userId);
281             try {
282                 root.docId = getDocIdForFile(root.path);
283             } catch (FileNotFoundException e) {
284                 throw new IllegalStateException(e);
285             }
286         }
287 
288         Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
289 
290         // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
291         // as well as content://com.android.externalstorage.documents/document/*/children,
292         // so just notify on content://com.android.externalstorage.documents/.
293         getContext().getContentResolver().notifyChange(BASE_URI, null, false);
294     }
295 
resolveRootProjection(String[] projection)296     private static String[] resolveRootProjection(String[] projection) {
297         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
298     }
299 
300     /**
301      * Mark {@code Android/data/}, {@code Android/obb/} and {@code Android/sandbox/} on the
302      * integrated shared ("external") storage along with all their content and subdirectories as
303      * hidden.
304      */
305     @Override
shouldHideDocument(@onNull String documentId)306     protected boolean shouldHideDocument(@NonNull String documentId) {
307         // Don't need to hide anything on USB drives.
308         if (isOnRemovableUsbStorage(documentId)) {
309             return false;
310         }
311 
312         final String path = getPathFromDocId(documentId);
313         return PATTERN_RESTRICTED_ANDROID_SUBTREES.matcher(path).matches();
314     }
315 
316     /**
317      * Check that the directory is the root of storage or blocked file from tree.
318      * <p>
319      * Note, that this is different from hidden documents: blocked documents <b>WILL</b> appear
320      * the UI, but the user <b>WILL NOT</b> be able to select them.
321      *
322      * @param documentId the docId of the directory to be checked
323      * @return true, should be blocked from tree. Otherwise, false.
324      *
325      * @see Document#FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE
326      */
327     @Override
shouldBlockDirectoryFromTree(@onNull String documentId)328     protected boolean shouldBlockDirectoryFromTree(@NonNull String documentId)
329             throws FileNotFoundException {
330         final File dir = getFileForDocId(documentId, false);
331         // The file is null or it is not a directory
332         if (dir == null || !dir.isDirectory()) {
333             return false;
334         }
335 
336         // Allow all directories on USB, including the root.
337         if (isOnRemovableUsbStorage(documentId)) {
338             return false;
339         }
340 
341         // Get canonical(!) path. Note that this path will have neither leading nor training "/".
342         // This the root's path will be just an empty string.
343         final String path = getPathFromDocId(documentId);
344 
345         // Block the root of the storage
346         if (path.isEmpty()) {
347             return true;
348         }
349 
350         // Block /Download/ and /Android/ folders from the tree.
351         if (equalIgnoringCase(path, Environment.DIRECTORY_DOWNLOADS) ||
352                 equalIgnoringCase(path, Environment.DIRECTORY_ANDROID)) {
353             return true;
354         }
355 
356         // This shouldn't really make a difference, but just in case - let's block hidden
357         // directories as well.
358         if (shouldHideDocument(documentId)) {
359             return true;
360         }
361 
362         return false;
363     }
364 
isOnRemovableUsbStorage(@onNull String documentId)365     private boolean isOnRemovableUsbStorage(@NonNull String documentId) {
366         final RootInfo rootInfo;
367         try {
368             rootInfo = getRootFromDocId(documentId);
369         } catch (FileNotFoundException e) {
370             Log.e(TAG, "Failed to determine rootInfo for docId\"" + documentId + '"');
371             return false;
372         }
373 
374         return (rootInfo.flags & Root.FLAG_REMOVABLE_USB) != 0;
375     }
376 
377     @NonNull
378     @Override
getDocIdForFile(@onNull File file)379     protected String getDocIdForFile(@NonNull File file) throws FileNotFoundException {
380         return getDocIdForFileMaybeCreate(file, false);
381     }
382 
383     @NonNull
getDocIdForFileMaybeCreate(@onNull File file, boolean createNewDir)384     private String getDocIdForFileMaybeCreate(@NonNull File file, boolean createNewDir)
385             throws FileNotFoundException {
386         String path = file.getAbsolutePath();
387 
388         // Find the most-specific root path
389         boolean visiblePath = false;
390         RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
391 
392         if (mostSpecificRoot == null) {
393             // Try visible path if no internal path matches. MediaStore uses visible paths.
394             visiblePath = true;
395             mostSpecificRoot = getMostSpecificRootForPath(path, true);
396         }
397 
398         if (mostSpecificRoot == null) {
399             throw new FileNotFoundException("Failed to find root that contains " + path);
400         }
401 
402         // Start at first char of path under root
403         final String rootPath = visiblePath
404                 ? mostSpecificRoot.visiblePath.getAbsolutePath()
405                 : mostSpecificRoot.path.getAbsolutePath();
406         if (rootPath.equals(path)) {
407             path = "";
408         } else if (rootPath.endsWith("/")) {
409             path = path.substring(rootPath.length());
410         } else {
411             path = path.substring(rootPath.length() + 1);
412         }
413 
414         if (!file.exists() && createNewDir) {
415             Log.i(TAG, "Creating new directory " + file);
416             if (!file.mkdir()) {
417                 Log.e(TAG, "Could not create directory " + file);
418             }
419         }
420 
421         return mostSpecificRoot.rootId + ':' + path;
422     }
423 
getMostSpecificRootForPath(String path, boolean visible)424     private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
425         // Find the most-specific root path
426         RootInfo mostSpecificRoot = null;
427         String mostSpecificPath = null;
428         synchronized (mRootsLock) {
429             for (int i = 0; i < mRoots.size(); i++) {
430                 final RootInfo root = mRoots.valueAt(i);
431                 final File rootFile = visible ? root.visiblePath : root.path;
432                 if (rootFile != null) {
433                     final String rootPath = rootFile.getAbsolutePath();
434                     if (path.startsWith(rootPath) && (mostSpecificPath == null
435                             || rootPath.length() > mostSpecificPath.length())) {
436                         mostSpecificRoot = root;
437                         mostSpecificPath = rootPath;
438                     }
439                 }
440             }
441         }
442 
443         return mostSpecificRoot;
444     }
445 
446     @Override
getFileForDocId(String docId, boolean visible)447     protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
448         return getFileForDocId(docId, visible, true);
449     }
450 
getFileForDocId(String docId, boolean visible, boolean mustExist)451     private File getFileForDocId(String docId, boolean visible, boolean mustExist)
452             throws FileNotFoundException {
453         RootInfo root = getRootFromDocId(docId);
454         return buildFile(root, docId, mustExist);
455     }
456 
resolveDocId(String docId)457     private Pair<RootInfo, File> resolveDocId(String docId) throws FileNotFoundException {
458         RootInfo root = getRootFromDocId(docId);
459         return Pair.create(root, buildFile(root, docId, /* mustExist */ true));
460     }
461 
462     @VisibleForTesting
getPathFromDocId(String docId)463     static String getPathFromDocId(String docId) {
464         final int splitIndex = docId.indexOf(':', 1);
465         final String docIdPath = docId.substring(splitIndex + 1);
466 
467         // Canonicalize path and strip the leading "/"
468         final String path;
469         try {
470             path = new File(docIdPath).getCanonicalPath().substring(1);
471         } catch (IOException e) {
472             Log.w(TAG, "Could not canonicalize \"" + docIdPath + '"');
473             return "";
474         }
475 
476         // Remove the trailing "/" as well.
477         if (!path.isEmpty() && path.charAt(path.length() - 1) == '/') {
478             return path.substring(0, path.length() - 1);
479         } else {
480             return path;
481         }
482     }
483 
getRootFromDocId(String docId)484     private RootInfo getRootFromDocId(String docId) throws FileNotFoundException {
485         final int splitIndex = docId.indexOf(':', 1);
486         final String tag = docId.substring(0, splitIndex);
487 
488         RootInfo root;
489         synchronized (mRootsLock) {
490             root = mRoots.get(tag);
491         }
492         if (root == null) {
493             throw new FileNotFoundException("No root for " + tag);
494         }
495 
496         return root;
497     }
498 
buildFile(RootInfo root, String docId, boolean mustExist)499     private File buildFile(RootInfo root, String docId, boolean mustExist)
500             throws FileNotFoundException {
501         final int splitIndex = docId.indexOf(':', 1);
502         final String path = docId.substring(splitIndex + 1);
503 
504         File target = root.visiblePath != null ? root.visiblePath : root.path;
505         if (target == null) {
506             return null;
507         }
508         if (!target.exists()) {
509             target.mkdirs();
510         }
511         try {
512             target = new File(target, path).getCanonicalFile();
513         } catch (IOException e) {
514             throw new FileNotFoundException("Failed to canonicalize path " + path);
515         }
516 
517         if (mustExist && !target.exists()) {
518             throw new FileNotFoundException("Missing file for " + docId + " at " + target);
519         }
520         return target;
521     }
522 
523     @Override
buildNotificationUri(String docId)524     protected Uri buildNotificationUri(String docId) {
525         return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
526     }
527 
528     @Override
onDocIdChanged(String docId)529     protected void onDocIdChanged(String docId) {
530         try {
531             // Touch the visible path to ensure that any sdcardfs caches have
532             // been updated to reflect underlying changes on disk.
533             final File visiblePath = getFileForDocId(docId, true, false);
534             if (visiblePath != null) {
535                 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK);
536             }
537         } catch (FileNotFoundException | ErrnoException ignored) {
538         }
539     }
540 
541     @Override
onDocIdDeleted(String docId)542     protected void onDocIdDeleted(String docId) {
543         Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, docId);
544         getContext().revokeUriPermission(uri, ~0);
545     }
546 
547 
548     @Override
queryRoots(String[] projection)549     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
550         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
551         synchronized (mRootsLock) {
552             for (RootInfo root : mRoots.values()) {
553                 final RowBuilder row = result.newRow();
554                 row.add(Root.COLUMN_ROOT_ID, root.rootId);
555                 row.add(Root.COLUMN_FLAGS, root.flags);
556                 row.add(Root.COLUMN_TITLE, root.title);
557                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
558                 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
559 
560                 long availableBytes = -1;
561                 if (root.reportAvailableBytes) {
562                     if (root.storageUuid != null) {
563                         try {
564                             availableBytes = getContext()
565                                     .getSystemService(StorageStatsManager.class)
566                                     .getFreeBytes(root.storageUuid);
567                         } catch (IOException e) {
568                             Log.w(TAG, e);
569                         }
570                     } else {
571                         availableBytes = root.path.getUsableSpace();
572                     }
573                 }
574                 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
575             }
576         }
577         return result;
578     }
579 
580     @Override
findDocumentPath(@ullable String parentDocId, String childDocId)581     public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
582             throws FileNotFoundException {
583         final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId);
584         final RootInfo root = resolvedDocId.first;
585         File child = resolvedDocId.second;
586 
587         final File rootFile = root.visiblePath != null ? root.visiblePath
588                 : root.path;
589         final File parent = TextUtils.isEmpty(parentDocId)
590                 ? rootFile
591                 : getFileForDocId(parentDocId);
592 
593         return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child));
594     }
595 
getDocumentUri(String path, List<UriPermission> accessUriPermissions)596     private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions)
597             throws FileNotFoundException {
598         File doc = new File(path);
599 
600         final String docId = getDocIdForFile(doc);
601 
602         UriPermission docUriPermission = null;
603         UriPermission treeUriPermission = null;
604         for (UriPermission uriPermission : accessUriPermissions) {
605             final Uri uri = uriPermission.getUri();
606             if (AUTHORITY.equals(uri.getAuthority())) {
607                 boolean matchesRequestedDoc = false;
608                 if (DocumentsContract.isTreeUri(uri)) {
609                     final String parentDocId = DocumentsContract.getTreeDocumentId(uri);
610                     if (isChildDocument(parentDocId, docId)) {
611                         treeUriPermission = uriPermission;
612                         matchesRequestedDoc = true;
613                     }
614                 } else {
615                     final String candidateDocId = DocumentsContract.getDocumentId(uri);
616                     if (Objects.equals(docId, candidateDocId)) {
617                         docUriPermission = uriPermission;
618                         matchesRequestedDoc = true;
619                     }
620                 }
621 
622                 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) {
623                     // This URI permission provides everything an app can get, no need to
624                     // further check any other granted URI.
625                     break;
626                 }
627             }
628         }
629 
630         // Full permission URI first.
631         if (allowsBothReadAndWrite(treeUriPermission)) {
632             return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
633         }
634 
635         if (allowsBothReadAndWrite(docUriPermission)) {
636             return docUriPermission.getUri();
637         }
638 
639         // Then partial permission URI.
640         if (treeUriPermission != null) {
641             return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId);
642         }
643 
644         if (docUriPermission != null) {
645             return docUriPermission.getUri();
646         }
647 
648         throw new SecurityException("The app is not given any access to the document under path " +
649                 path + " with permissions granted in " + accessUriPermissions);
650     }
651 
allowsBothReadAndWrite(UriPermission permission)652     private static boolean allowsBothReadAndWrite(UriPermission permission) {
653         return permission != null
654                 && permission.isReadPermission()
655                 && permission.isWritePermission();
656     }
657 
658     @Override
querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)659     public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
660             throws FileNotFoundException {
661         final File parent;
662 
663         synchronized (mRootsLock) {
664             RootInfo root = mRoots.get(rootId);
665             parent = root.visiblePath != null ? root.visiblePath
666                 : root.path;
667         }
668 
669         return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs);
670     }
671 
672     @Override
ejectRoot(String rootId)673     public void ejectRoot(String rootId) {
674         final long token = Binder.clearCallingIdentity();
675         RootInfo root = mRoots.get(rootId);
676         if (root != null) {
677             try {
678                 mStorageManager.unmount(root.volumeId);
679             } catch (RuntimeException e) {
680                 throw new IllegalStateException(e);
681             } finally {
682                 Binder.restoreCallingIdentity(token);
683             }
684         }
685     }
686 
687     /**
688      * Print the state into the given stream.
689      * Gets invoked when you run:
690      * <pre>
691      * adb shell dumpsys activity provider com.android.externalstorage/.ExternalStorageProvider
692      * </pre>
693      */
694     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)695     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
696         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
697         synchronized (mRootsLock) {
698             for (int i = 0; i < mRoots.size(); i++) {
699                 final RootInfo root = mRoots.valueAt(i);
700                 pw.println("Root{" + root.rootId + "}:");
701                 pw.increaseIndent();
702                 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
703                 pw.println();
704                 pw.printPair("title", root.title);
705                 pw.printPair("docId", root.docId);
706                 pw.println();
707                 pw.printPair("path", root.path);
708                 pw.printPair("visiblePath", root.visiblePath);
709                 pw.decreaseIndent();
710                 pw.println();
711             }
712         }
713     }
714 
715     @Override
call(String method, String arg, Bundle extras)716     public Bundle call(String method, String arg, Bundle extras) {
717         Bundle bundle = super.call(method, arg, extras);
718         if (bundle == null && !TextUtils.isEmpty(method)) {
719             switch (method) {
720                 case "getDocIdForFileCreateNewDir": {
721                     getContext().enforceCallingPermission(
722                             android.Manifest.permission.MANAGE_DOCUMENTS, null);
723                     if (TextUtils.isEmpty(arg)) {
724                         return null;
725                     }
726                     try {
727                         final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
728                         bundle = new Bundle();
729                         bundle.putString("DOC_ID", docId);
730                     } catch (FileNotFoundException e) {
731                         Log.w(TAG, "file '" + arg + "' not found");
732                         return null;
733                     }
734                     break;
735                 }
736                 case GET_DOCUMENT_URI_CALL: {
737                     // All callers must go through MediaProvider
738                     getContext().enforceCallingPermission(
739                             android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
740 
741                     final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
742                     final List<UriPermission> accessUriPermissions = extras
743                             .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS);
744 
745                     final String path = fileUri.getPath();
746                     try {
747                         final Bundle out = new Bundle();
748                         final Uri uri = getDocumentUri(path, accessUriPermissions);
749                         out.putParcelable(DocumentsContract.EXTRA_URI, uri);
750                         return out;
751                     } catch (FileNotFoundException e) {
752                         throw new IllegalStateException("File in " + path + " is not found.", e);
753                     }
754                 }
755                 case GET_MEDIA_URI_CALL: {
756                     // All callers must go through MediaProvider
757                     getContext().enforceCallingPermission(
758                             android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
759 
760                     final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
761                     final String docId = DocumentsContract.getDocumentId(documentUri);
762                     try {
763                         final Bundle out = new Bundle();
764                         final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
765                         out.putParcelable(DocumentsContract.EXTRA_URI, uri);
766                         return out;
767                     } catch (FileNotFoundException e) {
768                         throw new IllegalStateException(e);
769                     }
770                 }
771                 default:
772                     Log.w(TAG, "unknown method passed to call(): " + method);
773             }
774         }
775         return bundle;
776     }
777 
equalIgnoringCase(@onNull String a, @NonNull String b)778     private static boolean equalIgnoringCase(@NonNull String a, @NonNull String b) {
779         return TextUtils.equals(a.toLowerCase(Locale.ROOT), b.toLowerCase(Locale.ROOT));
780     }
781 }
782