• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.ContentResolver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.res.AssetFileDescriptor;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MatrixCursor.RowBuilder;
26 import android.graphics.Point;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.os.Environment;
31 import android.os.FileObserver;
32 import android.os.FileUtils;
33 import android.os.Handler;
34 import android.os.ParcelFileDescriptor;
35 import android.os.ParcelFileDescriptor.OnCloseListener;
36 import android.os.UserHandle;
37 import android.os.storage.DiskInfo;
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.Root;
43 import android.provider.DocumentsProvider;
44 import android.provider.MediaStore;
45 import android.provider.Settings;
46 import android.support.provider.DocumentArchiveHelper;
47 import android.text.TextUtils;
48 import android.util.ArrayMap;
49 import android.util.DebugUtils;
50 import android.util.Log;
51 import android.webkit.MimeTypeMap;
52 
53 import com.android.internal.annotations.GuardedBy;
54 import com.android.internal.util.IndentingPrintWriter;
55 
56 import java.io.File;
57 import java.io.FileDescriptor;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.PrintWriter;
61 import java.util.LinkedList;
62 import java.util.List;
63 
64 public class ExternalStorageProvider extends DocumentsProvider {
65     private static final String TAG = "ExternalStorage";
66 
67     private static final boolean DEBUG = false;
68     private static final boolean LOG_INOTIFY = false;
69 
70     public static final String AUTHORITY = "com.android.externalstorage.documents";
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,
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 int flags;
90         public String title;
91         public String docId;
92         public File visiblePath;
93         public File path;
94         public boolean reportAvailableBytes = true;
95     }
96 
97     private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
98     private static final String ROOT_ID_HOME = "home";
99 
100     private StorageManager mStorageManager;
101     private Handler mHandler;
102     private DocumentArchiveHelper mArchiveHelper;
103 
104     private final Object mRootsLock = new Object();
105 
106     @GuardedBy("mRootsLock")
107     private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
108 
109     @GuardedBy("mObservers")
110     private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
111 
112     @Override
onCreate()113     public boolean onCreate() {
114         mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
115         mHandler = new Handler();
116         mArchiveHelper = new DocumentArchiveHelper(this, (char) 0);
117 
118         updateVolumes();
119         return true;
120     }
121 
updateVolumes()122     public void updateVolumes() {
123         synchronized (mRootsLock) {
124             updateVolumesLocked();
125         }
126     }
127 
updateVolumesLocked()128     private void updateVolumesLocked() {
129         mRoots.clear();
130 
131         VolumeInfo primaryVolume = null;
132         final int userId = UserHandle.myUserId();
133         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
134         for (VolumeInfo volume : volumes) {
135             if (!volume.isMountedReadable()) continue;
136 
137             final String rootId;
138             final String title;
139             if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
140                 // We currently only support a single emulated volume mounted at
141                 // a time, and it's always considered the primary
142                 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume);
143                 rootId = ROOT_ID_PRIMARY_EMULATED;
144 
145                 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
146                     // This is basically the user's primary device storage.
147                     // Use device name for the volume since this is likely same thing
148                     // the user sees when they mount their phone on another device.
149                     String deviceName = Settings.Global.getString(
150                             getContext().getContentResolver(), Settings.Global.DEVICE_NAME);
151 
152                     // Device name should always be set. In case it isn't, though,
153                     // fall back to a localized "Internal Storage" string.
154                     title = !TextUtils.isEmpty(deviceName)
155                             ? deviceName
156                             : getContext().getString(R.string.root_internal_storage);
157                 } else {
158                     // This should cover all other storage devices, like an SD card
159                     // or USB OTG drive plugged in. Using getBestVolumeDescription()
160                     // will give us a nice string like "Samsung SD card" or "SanDisk USB drive"
161                     final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
162                     title = mStorageManager.getBestVolumeDescription(privateVol);
163                 }
164             } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
165                 rootId = volume.getFsUuid();
166                 title = mStorageManager.getBestVolumeDescription(volume);
167             } else {
168                 // Unsupported volume; ignore
169                 continue;
170             }
171 
172             if (TextUtils.isEmpty(rootId)) {
173                 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
174                 continue;
175             }
176             if (mRoots.containsKey(rootId)) {
177                 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
178                 continue;
179             }
180 
181             final RootInfo root = new RootInfo();
182             mRoots.put(rootId, root);
183 
184             root.rootId = rootId;
185             root.flags = Root.FLAG_LOCAL_ONLY
186                     | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
187 
188             final DiskInfo disk = volume.getDisk();
189             if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
190             if (disk != null && disk.isSd()) {
191                 root.flags |= Root.FLAG_REMOVABLE_SD;
192             } else if (disk != null && disk.isUsb()) {
193                 root.flags |= Root.FLAG_REMOVABLE_USB;
194             }
195 
196             if (volume.isPrimary()) {
197                 // save off the primary volume for subsequent "Home" dir initialization.
198                 primaryVolume = volume;
199                 root.flags |= Root.FLAG_ADVANCED;
200             }
201             // Dunno when this would NOT be the case, but never hurts to be correct.
202             if (volume.isMountedWritable()) {
203                 root.flags |= Root.FLAG_SUPPORTS_CREATE;
204             }
205             root.title = title;
206             if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
207                 root.flags |= Root.FLAG_HAS_SETTINGS;
208             }
209             if (volume.isVisibleForRead(userId)) {
210                 root.visiblePath = volume.getPathForUser(userId);
211             } else {
212                 root.visiblePath = null;
213             }
214             root.path = volume.getInternalPathForUser(userId);
215             try {
216                 root.docId = getDocIdForFile(root.path);
217             } catch (FileNotFoundException e) {
218                 throw new IllegalStateException(e);
219             }
220         }
221 
222         // Finally, if primary storage is available we add the "Documents" directory.
223         // If I recall correctly the actual directory is created on demand
224         // by calling either getPathForUser, or getInternalPathForUser.
225         if (primaryVolume != null && primaryVolume.isVisible()) {
226             final RootInfo root = new RootInfo();
227             root.rootId = ROOT_ID_HOME;
228             mRoots.put(root.rootId, root);
229             root.title = getContext().getString(R.string.root_documents);
230 
231             // Only report bytes on *volumes*...as a matter of policy.
232             root.reportAvailableBytes = false;
233             root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH
234                     | Root.FLAG_SUPPORTS_IS_CHILD;
235 
236             // Dunno when this would NOT be the case, but never hurts to be correct.
237             if (primaryVolume.isMountedWritable()) {
238                 root.flags |= Root.FLAG_SUPPORTS_CREATE;
239             }
240 
241             // Create the "Documents" directory on disk (don't use the localized title).
242             root.visiblePath = new File(
243                     primaryVolume.getPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
244             root.path = new File(
245                     primaryVolume.getInternalPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
246             try {
247                 root.docId = getDocIdForFile(root.path);
248             } catch (FileNotFoundException e) {
249                 throw new IllegalStateException(e);
250             }
251         }
252 
253         Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
254 
255         // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
256         // as well as content://com.android.externalstorage.documents/document/*/children,
257         // so just notify on content://com.android.externalstorage.documents/.
258         getContext().getContentResolver().notifyChange(BASE_URI, null, false);
259     }
260 
resolveRootProjection(String[] projection)261     private static String[] resolveRootProjection(String[] projection) {
262         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
263     }
264 
resolveDocumentProjection(String[] projection)265     private static String[] resolveDocumentProjection(String[] projection) {
266         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
267     }
268 
269 
getDocIdForFile(File file)270     private String getDocIdForFile(File file) throws FileNotFoundException {
271         return getDocIdForFileMaybeCreate(file, false);
272     }
273 
getDocIdForFileMaybeCreate(File file, boolean createNewDir)274     private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
275             throws FileNotFoundException {
276         String path = file.getAbsolutePath();
277 
278         // Find the most-specific root path
279         String mostSpecificId = null;
280         String mostSpecificPath = null;
281         synchronized (mRootsLock) {
282             for (int i = 0; i < mRoots.size(); i++) {
283                 final String rootId = mRoots.keyAt(i);
284                 final String rootPath = mRoots.valueAt(i).path.getAbsolutePath();
285                 if (path.startsWith(rootPath) && (mostSpecificPath == null
286                         || rootPath.length() > mostSpecificPath.length())) {
287                     mostSpecificId = rootId;
288                     mostSpecificPath = rootPath;
289                 }
290             }
291         }
292 
293         if (mostSpecificPath == null) {
294             throw new FileNotFoundException("Failed to find root that contains " + path);
295         }
296 
297         // Start at first char of path under root
298         final String rootPath = mostSpecificPath;
299         if (rootPath.equals(path)) {
300             path = "";
301         } else if (rootPath.endsWith("/")) {
302             path = path.substring(rootPath.length());
303         } else {
304             path = path.substring(rootPath.length() + 1);
305         }
306 
307         if (!file.exists() && createNewDir) {
308             Log.i(TAG, "Creating new directory " + file);
309             if (!file.mkdir()) {
310                 Log.e(TAG, "Could not create directory " + file);
311             }
312         }
313 
314         return mostSpecificId + ':' + path;
315     }
316 
getFileForDocId(String docId)317     private File getFileForDocId(String docId) throws FileNotFoundException {
318         return getFileForDocId(docId, false);
319     }
320 
getFileForDocId(String docId, boolean visible)321     private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
322         final int splitIndex = docId.indexOf(':', 1);
323         final String tag = docId.substring(0, splitIndex);
324         final String path = docId.substring(splitIndex + 1);
325 
326         RootInfo root;
327         synchronized (mRootsLock) {
328             root = mRoots.get(tag);
329         }
330         if (root == null) {
331             throw new FileNotFoundException("No root for " + tag);
332         }
333 
334         File target = visible ? root.visiblePath : root.path;
335         if (target == null) {
336             return null;
337         }
338         if (!target.exists()) {
339             target.mkdirs();
340         }
341         target = new File(target, path);
342         if (!target.exists()) {
343             throw new FileNotFoundException("Missing file for " + docId + " at " + target);
344         }
345         return target;
346     }
347 
includeFile(MatrixCursor result, String docId, File file)348     private void includeFile(MatrixCursor result, String docId, File file)
349             throws FileNotFoundException {
350         if (docId == null) {
351             docId = getDocIdForFile(file);
352         } else {
353             file = getFileForDocId(docId);
354         }
355 
356         int flags = 0;
357 
358         if (file.canWrite()) {
359             if (file.isDirectory()) {
360                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
361                 flags |= Document.FLAG_SUPPORTS_DELETE;
362                 flags |= Document.FLAG_SUPPORTS_RENAME;
363                 flags |= Document.FLAG_SUPPORTS_MOVE;
364             } else {
365                 flags |= Document.FLAG_SUPPORTS_WRITE;
366                 flags |= Document.FLAG_SUPPORTS_DELETE;
367                 flags |= Document.FLAG_SUPPORTS_RENAME;
368                 flags |= Document.FLAG_SUPPORTS_MOVE;
369             }
370         }
371 
372         final String mimeType = getTypeForFile(file);
373         if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
374             flags |= Document.FLAG_ARCHIVE;
375         }
376 
377         final String displayName = file.getName();
378         if (mimeType.startsWith("image/")) {
379             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
380         }
381 
382         final RowBuilder row = result.newRow();
383         row.add(Document.COLUMN_DOCUMENT_ID, docId);
384         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
385         row.add(Document.COLUMN_SIZE, file.length());
386         row.add(Document.COLUMN_MIME_TYPE, mimeType);
387         row.add(Document.COLUMN_FLAGS, flags);
388         row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, file.getPath());
389 
390         // Only publish dates reasonably after epoch
391         long lastModified = file.lastModified();
392         if (lastModified > 31536000000L) {
393             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
394         }
395     }
396 
397     @Override
queryRoots(String[] projection)398     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
399         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
400         synchronized (mRootsLock) {
401             for (RootInfo root : mRoots.values()) {
402                 final RowBuilder row = result.newRow();
403                 row.add(Root.COLUMN_ROOT_ID, root.rootId);
404                 row.add(Root.COLUMN_FLAGS, root.flags);
405                 row.add(Root.COLUMN_TITLE, root.title);
406                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
407                 row.add(Root.COLUMN_AVAILABLE_BYTES,
408                         root.reportAvailableBytes ? root.path.getFreeSpace() : -1);
409             }
410         }
411         return result;
412     }
413 
414     @Override
isChildDocument(String parentDocId, String docId)415     public boolean isChildDocument(String parentDocId, String docId) {
416         try {
417             if (mArchiveHelper.isArchivedDocument(docId)) {
418                 return mArchiveHelper.isChildDocument(parentDocId, docId);
419             }
420             // Archives do not contain regular files.
421             if (mArchiveHelper.isArchivedDocument(parentDocId)) {
422                 return false;
423             }
424 
425             final File parent = getFileForDocId(parentDocId).getCanonicalFile();
426             final File doc = getFileForDocId(docId).getCanonicalFile();
427             return FileUtils.contains(parent, doc);
428         } catch (IOException e) {
429             throw new IllegalArgumentException(
430                     "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
431         }
432     }
433 
434     @Override
createDocument(String docId, String mimeType, String displayName)435     public String createDocument(String docId, String mimeType, String displayName)
436             throws FileNotFoundException {
437         displayName = FileUtils.buildValidFatFilename(displayName);
438 
439         final File parent = getFileForDocId(docId);
440         if (!parent.isDirectory()) {
441             throw new IllegalArgumentException("Parent document isn't a directory");
442         }
443 
444         final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
445         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
446             if (!file.mkdir()) {
447                 throw new IllegalStateException("Failed to mkdir " + file);
448             }
449         } else {
450             try {
451                 if (!file.createNewFile()) {
452                     throw new IllegalStateException("Failed to touch " + file);
453                 }
454             } catch (IOException e) {
455                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
456             }
457         }
458 
459         return getDocIdForFile(file);
460     }
461 
462     @Override
renameDocument(String docId, String displayName)463     public String renameDocument(String docId, String displayName) throws FileNotFoundException {
464         // Since this provider treats renames as generating a completely new
465         // docId, we're okay with letting the MIME type change.
466         displayName = FileUtils.buildValidFatFilename(displayName);
467 
468         final File before = getFileForDocId(docId);
469         final File after = new File(before.getParentFile(), displayName);
470         if (after.exists()) {
471             throw new IllegalStateException("Already exists " + after);
472         }
473         if (!before.renameTo(after)) {
474             throw new IllegalStateException("Failed to rename to " + after);
475         }
476         final String afterDocId = getDocIdForFile(after);
477         if (!TextUtils.equals(docId, afterDocId)) {
478             return afterDocId;
479         } else {
480             return null;
481         }
482     }
483 
484     @Override
deleteDocument(String docId)485     public void deleteDocument(String docId) throws FileNotFoundException {
486         final File file = getFileForDocId(docId);
487         final File visibleFile = getFileForDocId(docId, true);
488 
489         final boolean isDirectory = file.isDirectory();
490         if (isDirectory) {
491             FileUtils.deleteContents(file);
492         }
493         if (!file.delete()) {
494             throw new IllegalStateException("Failed to delete " + file);
495         }
496 
497         if (visibleFile != null) {
498             final ContentResolver resolver = getContext().getContentResolver();
499             final Uri externalUri = MediaStore.Files.getContentUri("external");
500 
501             // Remove media store entries for any files inside this directory, using
502             // path prefix match. Logic borrowed from MtpDatabase.
503             if (isDirectory) {
504                 final String path = visibleFile.getAbsolutePath() + "/";
505                 resolver.delete(externalUri,
506                         "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
507                         new String[] { path + "%", Integer.toString(path.length()), path });
508             }
509 
510             // Remove media store entry for this exact file.
511             final String path = visibleFile.getAbsolutePath();
512             resolver.delete(externalUri,
513                     "_data LIKE ?1 AND lower(_data)=lower(?2)",
514                     new String[] { path, path });
515         }
516     }
517 
518     @Override
moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)519     public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
520             String targetParentDocumentId)
521             throws FileNotFoundException {
522         final File before = getFileForDocId(sourceDocumentId);
523         final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
524 
525         if (after.exists()) {
526             throw new IllegalStateException("Already exists " + after);
527         }
528         if (!before.renameTo(after)) {
529             throw new IllegalStateException("Failed to move to " + after);
530         }
531         return getDocIdForFile(after);
532     }
533 
534     @Override
queryDocument(String documentId, String[] projection)535     public Cursor queryDocument(String documentId, String[] projection)
536             throws FileNotFoundException {
537         if (mArchiveHelper.isArchivedDocument(documentId)) {
538             return mArchiveHelper.queryDocument(documentId, projection);
539         }
540 
541         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
542         includeFile(result, documentId, null);
543         return result;
544     }
545 
546     @Override
queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)547     public Cursor queryChildDocuments(
548             String parentDocumentId, String[] projection, String sortOrder)
549             throws FileNotFoundException {
550         if (mArchiveHelper.isArchivedDocument(parentDocumentId) ||
551                 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) {
552             return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder);
553         }
554 
555         final File parent = getFileForDocId(parentDocumentId);
556         final MatrixCursor result = new DirectoryCursor(
557                 resolveDocumentProjection(projection), parentDocumentId, parent);
558         for (File file : parent.listFiles()) {
559             includeFile(result, null, file);
560         }
561         return result;
562     }
563 
564     @Override
querySearchDocuments(String rootId, String query, String[] projection)565     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
566             throws FileNotFoundException {
567         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
568 
569         final File parent;
570         synchronized (mRootsLock) {
571             parent = mRoots.get(rootId).path;
572         }
573 
574         final LinkedList<File> pending = new LinkedList<File>();
575         pending.add(parent);
576         while (!pending.isEmpty() && result.getCount() < 24) {
577             final File file = pending.removeFirst();
578             if (file.isDirectory()) {
579                 for (File child : file.listFiles()) {
580                     pending.add(child);
581                 }
582             }
583             if (file.getName().toLowerCase().contains(query)) {
584                 includeFile(result, null, file);
585             }
586         }
587         return result;
588     }
589 
590     @Override
getDocumentType(String documentId)591     public String getDocumentType(String documentId) throws FileNotFoundException {
592         if (mArchiveHelper.isArchivedDocument(documentId)) {
593             return mArchiveHelper.getDocumentType(documentId);
594         }
595 
596         final File file = getFileForDocId(documentId);
597         return getTypeForFile(file);
598     }
599 
600     @Override
openDocument( String documentId, String mode, CancellationSignal signal)601     public ParcelFileDescriptor openDocument(
602             String documentId, String mode, CancellationSignal signal)
603             throws FileNotFoundException {
604         if (mArchiveHelper.isArchivedDocument(documentId)) {
605             return mArchiveHelper.openDocument(documentId, mode, signal);
606         }
607 
608         final File file = getFileForDocId(documentId);
609         final File visibleFile = getFileForDocId(documentId, true);
610 
611         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
612         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
613             return ParcelFileDescriptor.open(file, pfdMode);
614         } else {
615             try {
616                 // When finished writing, kick off media scanner
617                 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
618                     @Override
619                     public void onClose(IOException e) {
620                         final Intent intent = new Intent(
621                                 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
622                         intent.setData(Uri.fromFile(visibleFile));
623                         getContext().sendBroadcast(intent);
624                     }
625                 });
626             } catch (IOException e) {
627                 throw new FileNotFoundException("Failed to open for writing: " + e);
628             }
629         }
630     }
631 
632     @Override
633     public AssetFileDescriptor openDocumentThumbnail(
634             String documentId, Point sizeHint, CancellationSignal signal)
635             throws FileNotFoundException {
636         if (mArchiveHelper.isArchivedDocument(documentId)) {
637             return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal);
638         }
639 
640         final File file = getFileForDocId(documentId);
641         return DocumentsContract.openImageThumbnail(file);
642     }
643 
644     @Override
645     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
646         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
647         synchronized (mRootsLock) {
648             for (int i = 0; i < mRoots.size(); i++) {
649                 final RootInfo root = mRoots.valueAt(i);
650                 pw.println("Root{" + root.rootId + "}:");
651                 pw.increaseIndent();
652                 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
653                 pw.println();
654                 pw.printPair("title", root.title);
655                 pw.printPair("docId", root.docId);
656                 pw.println();
657                 pw.printPair("path", root.path);
658                 pw.printPair("visiblePath", root.visiblePath);
659                 pw.decreaseIndent();
660                 pw.println();
661             }
662         }
663     }
664 
665     @Override
666     public Bundle call(String method, String arg, Bundle extras) {
667         Bundle bundle = super.call(method, arg, extras);
668         if (bundle == null && !TextUtils.isEmpty(method)) {
669             switch (method) {
670                 case "getDocIdForFileCreateNewDir": {
671                     getContext().enforceCallingPermission(
672                             android.Manifest.permission.MANAGE_DOCUMENTS, null);
673                     if (TextUtils.isEmpty(arg)) {
674                         return null;
675                     }
676                     try {
677                         final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
678                         bundle = new Bundle();
679                         bundle.putString("DOC_ID", docId);
680                     } catch (FileNotFoundException e) {
681                         Log.w(TAG, "file '" + arg + "' not found");
682                         return null;
683                     }
684                     break;
685                 }
686                 default:
687                     Log.w(TAG, "unknown method passed to call(): " + method);
688             }
689         }
690         return bundle;
691     }
692 
693     private static String getTypeForFile(File file) {
694         if (file.isDirectory()) {
695             return Document.MIME_TYPE_DIR;
696         } else {
697             return getTypeForName(file.getName());
698         }
699     }
700 
701     private static String getTypeForName(String name) {
702         final int lastDot = name.lastIndexOf('.');
703         if (lastDot >= 0) {
704             final String extension = name.substring(lastDot + 1).toLowerCase();
705             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
706             if (mime != null) {
707                 return mime;
708             }
709         }
710 
711         return "application/octet-stream";
712     }
713 
714     private void startObserving(File file, Uri notifyUri) {
715         synchronized (mObservers) {
716             DirectoryObserver observer = mObservers.get(file);
717             if (observer == null) {
718                 observer = new DirectoryObserver(
719                         file, getContext().getContentResolver(), notifyUri);
720                 observer.startWatching();
721                 mObservers.put(file, observer);
722             }
723             observer.mRefCount++;
724 
725             if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
726         }
727     }
728 
729     private void stopObserving(File file) {
730         synchronized (mObservers) {
731             DirectoryObserver observer = mObservers.get(file);
732             if (observer == null) return;
733 
734             observer.mRefCount--;
735             if (observer.mRefCount == 0) {
736                 mObservers.remove(file);
737                 observer.stopWatching();
738             }
739 
740             if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
741         }
742     }
743 
744     private static class DirectoryObserver extends FileObserver {
745         private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
746                 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
747 
748         private final File mFile;
749         private final ContentResolver mResolver;
750         private final Uri mNotifyUri;
751 
752         private int mRefCount = 0;
753 
754         public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
755             super(file.getAbsolutePath(), NOTIFY_EVENTS);
756             mFile = file;
757             mResolver = resolver;
758             mNotifyUri = notifyUri;
759         }
760 
761         @Override
762         public void onEvent(int event, String path) {
763             if ((event & NOTIFY_EVENTS) != 0) {
764                 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
765                 mResolver.notifyChange(mNotifyUri, null, false);
766             }
767         }
768 
769         @Override
770         public String toString() {
771             return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
772         }
773     }
774 
775     private class DirectoryCursor extends MatrixCursor {
776         private final File mFile;
777 
778         public DirectoryCursor(String[] columnNames, String docId, File file) {
779             super(columnNames);
780 
781             final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
782                     AUTHORITY, docId);
783             setNotificationUri(getContext().getContentResolver(), notifyUri);
784 
785             mFile = file;
786             startObserving(mFile, notifyUri);
787         }
788 
789         @Override
790         public void close() {
791             super.close();
792             stopObserving(mFile);
793         }
794     }
795 }
796