• 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.CancellationSignal;
29 import android.os.FileObserver;
30 import android.os.FileUtils;
31 import android.os.Handler;
32 import android.os.ParcelFileDescriptor;
33 import android.os.ParcelFileDescriptor.OnCloseListener;
34 import android.os.UserHandle;
35 import android.os.storage.StorageManager;
36 import android.os.storage.VolumeInfo;
37 import android.provider.DocumentsContract;
38 import android.provider.DocumentsContract.Document;
39 import android.provider.DocumentsContract.Root;
40 import android.provider.DocumentsProvider;
41 import android.provider.MediaStore;
42 import android.text.TextUtils;
43 import android.util.ArrayMap;
44 import android.util.DebugUtils;
45 import android.util.Log;
46 import android.webkit.MimeTypeMap;
47 
48 import com.android.internal.annotations.GuardedBy;
49 import com.android.internal.util.IndentingPrintWriter;
50 
51 import java.io.File;
52 import java.io.FileDescriptor;
53 import java.io.FileNotFoundException;
54 import java.io.IOException;
55 import java.io.PrintWriter;
56 import java.util.LinkedList;
57 import java.util.List;
58 
59 public class ExternalStorageProvider extends DocumentsProvider {
60     private static final String TAG = "ExternalStorage";
61 
62     private static final boolean LOG_INOTIFY = false;
63 
64     public static final String AUTHORITY = "com.android.externalstorage.documents";
65 
66     private static final Uri BASE_URI =
67             new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
68 
69     // docId format: root:path/to/file
70 
71     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
72             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
73             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
74     };
75 
76     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
77             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
78             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
79     };
80 
81     private static class RootInfo {
82         public String rootId;
83         public int flags;
84         public String title;
85         public String docId;
86         public File visiblePath;
87         public File path;
88     }
89 
90     private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
91 
92     private StorageManager mStorageManager;
93     private Handler mHandler;
94 
95     private final Object mRootsLock = new Object();
96 
97     @GuardedBy("mRootsLock")
98     private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
99 
100     @GuardedBy("mObservers")
101     private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
102 
103     @Override
onCreate()104     public boolean onCreate() {
105         mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
106         mHandler = new Handler();
107 
108         updateVolumes();
109         return true;
110     }
111 
updateVolumes()112     public void updateVolumes() {
113         synchronized (mRootsLock) {
114             updateVolumesLocked();
115         }
116     }
117 
updateVolumesLocked()118     private void updateVolumesLocked() {
119         mRoots.clear();
120 
121         final int userId = UserHandle.myUserId();
122         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
123         for (VolumeInfo volume : volumes) {
124             if (!volume.isMountedReadable()) continue;
125 
126             final String rootId;
127             final String title;
128             if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
129                 // We currently only support a single emulated volume mounted at
130                 // a time, and it's always considered the primary
131                 rootId = ROOT_ID_PRIMARY_EMULATED;
132                 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
133                     title = getContext().getString(R.string.root_internal_storage);
134                 } else {
135                     final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
136                     title = mStorageManager.getBestVolumeDescription(privateVol);
137                 }
138             } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
139                 rootId = volume.getFsUuid();
140                 title = mStorageManager.getBestVolumeDescription(volume);
141             } else {
142                 // Unsupported volume; ignore
143                 continue;
144             }
145 
146             if (TextUtils.isEmpty(rootId)) {
147                 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
148                 continue;
149             }
150             if (mRoots.containsKey(rootId)) {
151                 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
152                 continue;
153             }
154 
155             try {
156                 final RootInfo root = new RootInfo();
157                 mRoots.put(rootId, root);
158 
159                 root.rootId = rootId;
160                 root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
161                         | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
162                 root.title = title;
163                 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
164                     root.flags |= Root.FLAG_HAS_SETTINGS;
165                 }
166                 if (volume.isVisibleForRead(userId)) {
167                     root.visiblePath = volume.getPathForUser(userId);
168                 } else {
169                     root.visiblePath = null;
170                 }
171                 root.path = volume.getInternalPathForUser(userId);
172                 root.docId = getDocIdForFile(root.path);
173 
174             } catch (FileNotFoundException e) {
175                 throw new IllegalStateException(e);
176             }
177         }
178 
179         Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
180 
181         // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
182         // as well as content://com.android.externalstorage.documents/document/*/children,
183         // so just notify on content://com.android.externalstorage.documents/.
184         getContext().getContentResolver().notifyChange(BASE_URI, null, false);
185     }
186 
resolveRootProjection(String[] projection)187     private static String[] resolveRootProjection(String[] projection) {
188         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
189     }
190 
resolveDocumentProjection(String[] projection)191     private static String[] resolveDocumentProjection(String[] projection) {
192         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
193     }
194 
getDocIdForFile(File file)195     private String getDocIdForFile(File file) throws FileNotFoundException {
196         String path = file.getAbsolutePath();
197 
198         // Find the most-specific root path
199         String mostSpecificId = null;
200         String mostSpecificPath = null;
201         synchronized (mRootsLock) {
202             for (int i = 0; i < mRoots.size(); i++) {
203                 final String rootId = mRoots.keyAt(i);
204                 final String rootPath = mRoots.valueAt(i).path.getAbsolutePath();
205                 if (path.startsWith(rootPath) && (mostSpecificPath == null
206                         || rootPath.length() > mostSpecificPath.length())) {
207                     mostSpecificId = rootId;
208                     mostSpecificPath = rootPath;
209                 }
210             }
211         }
212 
213         if (mostSpecificPath == null) {
214             throw new FileNotFoundException("Failed to find root that contains " + path);
215         }
216 
217         // Start at first char of path under root
218         final String rootPath = mostSpecificPath;
219         if (rootPath.equals(path)) {
220             path = "";
221         } else if (rootPath.endsWith("/")) {
222             path = path.substring(rootPath.length());
223         } else {
224             path = path.substring(rootPath.length() + 1);
225         }
226 
227         return mostSpecificId + ':' + path;
228     }
229 
getFileForDocId(String docId)230     private File getFileForDocId(String docId) throws FileNotFoundException {
231         return getFileForDocId(docId, false);
232     }
233 
getFileForDocId(String docId, boolean visible)234     private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
235         final int splitIndex = docId.indexOf(':', 1);
236         final String tag = docId.substring(0, splitIndex);
237         final String path = docId.substring(splitIndex + 1);
238 
239         RootInfo root;
240         synchronized (mRootsLock) {
241             root = mRoots.get(tag);
242         }
243         if (root == null) {
244             throw new FileNotFoundException("No root for " + tag);
245         }
246 
247         File target = visible ? root.visiblePath : root.path;
248         if (target == null) {
249             return null;
250         }
251         if (!target.exists()) {
252             target.mkdirs();
253         }
254         target = new File(target, path);
255         if (!target.exists()) {
256             throw new FileNotFoundException("Missing file for " + docId + " at " + target);
257         }
258         return target;
259     }
260 
includeFile(MatrixCursor result, String docId, File file)261     private void includeFile(MatrixCursor result, String docId, File file)
262             throws FileNotFoundException {
263         if (docId == null) {
264             docId = getDocIdForFile(file);
265         } else {
266             file = getFileForDocId(docId);
267         }
268 
269         int flags = 0;
270 
271         if (file.canWrite()) {
272             if (file.isDirectory()) {
273                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
274                 flags |= Document.FLAG_SUPPORTS_DELETE;
275                 flags |= Document.FLAG_SUPPORTS_RENAME;
276             } else {
277                 flags |= Document.FLAG_SUPPORTS_WRITE;
278                 flags |= Document.FLAG_SUPPORTS_DELETE;
279                 flags |= Document.FLAG_SUPPORTS_RENAME;
280             }
281         }
282 
283         final String displayName = file.getName();
284         final String mimeType = getTypeForFile(file);
285         if (mimeType.startsWith("image/")) {
286             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
287         }
288 
289         final RowBuilder row = result.newRow();
290         row.add(Document.COLUMN_DOCUMENT_ID, docId);
291         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
292         row.add(Document.COLUMN_SIZE, file.length());
293         row.add(Document.COLUMN_MIME_TYPE, mimeType);
294         row.add(Document.COLUMN_FLAGS, flags);
295 
296         // Only publish dates reasonably after epoch
297         long lastModified = file.lastModified();
298         if (lastModified > 31536000000L) {
299             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
300         }
301     }
302 
303     @Override
queryRoots(String[] projection)304     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
305         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
306         synchronized (mRootsLock) {
307             for (RootInfo root : mRoots.values()) {
308                 final RowBuilder row = result.newRow();
309                 row.add(Root.COLUMN_ROOT_ID, root.rootId);
310                 row.add(Root.COLUMN_FLAGS, root.flags);
311                 row.add(Root.COLUMN_TITLE, root.title);
312                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
313                 row.add(Root.COLUMN_AVAILABLE_BYTES, root.path.getFreeSpace());
314             }
315         }
316         return result;
317     }
318 
319     @Override
isChildDocument(String parentDocId, String docId)320     public boolean isChildDocument(String parentDocId, String docId) {
321         try {
322             final File parent = getFileForDocId(parentDocId).getCanonicalFile();
323             final File doc = getFileForDocId(docId).getCanonicalFile();
324             return FileUtils.contains(parent, doc);
325         } catch (IOException e) {
326             throw new IllegalArgumentException(
327                     "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
328         }
329     }
330 
331     @Override
createDocument(String docId, String mimeType, String displayName)332     public String createDocument(String docId, String mimeType, String displayName)
333             throws FileNotFoundException {
334         displayName = FileUtils.buildValidFatFilename(displayName);
335 
336         final File parent = getFileForDocId(docId);
337         if (!parent.isDirectory()) {
338             throw new IllegalArgumentException("Parent document isn't a directory");
339         }
340 
341         final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
342         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
343             if (!file.mkdir()) {
344                 throw new IllegalStateException("Failed to mkdir " + file);
345             }
346         } else {
347             try {
348                 if (!file.createNewFile()) {
349                     throw new IllegalStateException("Failed to touch " + file);
350                 }
351             } catch (IOException e) {
352                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
353             }
354         }
355 
356         return getDocIdForFile(file);
357     }
358 
359     @Override
renameDocument(String docId, String displayName)360     public String renameDocument(String docId, String displayName) throws FileNotFoundException {
361         // Since this provider treats renames as generating a completely new
362         // docId, we're okay with letting the MIME type change.
363         displayName = FileUtils.buildValidFatFilename(displayName);
364 
365         final File before = getFileForDocId(docId);
366         final File after = new File(before.getParentFile(), displayName);
367         if (after.exists()) {
368             throw new IllegalStateException("Already exists " + after);
369         }
370         if (!before.renameTo(after)) {
371             throw new IllegalStateException("Failed to rename to " + after);
372         }
373         final String afterDocId = getDocIdForFile(after);
374         if (!TextUtils.equals(docId, afterDocId)) {
375             return afterDocId;
376         } else {
377             return null;
378         }
379     }
380 
381     @Override
deleteDocument(String docId)382     public void deleteDocument(String docId) throws FileNotFoundException {
383         final File file = getFileForDocId(docId);
384         final boolean isDirectory = file.isDirectory();
385         if (isDirectory) {
386             FileUtils.deleteContents(file);
387         }
388         if (!file.delete()) {
389             throw new IllegalStateException("Failed to delete " + file);
390         }
391 
392         final ContentResolver resolver = getContext().getContentResolver();
393         final Uri externalUri = MediaStore.Files.getContentUri("external");
394 
395         // Remove media store entries for any files inside this directory, using
396         // path prefix match. Logic borrowed from MtpDatabase.
397         if (isDirectory) {
398             final String path = file.getAbsolutePath() + "/";
399             resolver.delete(externalUri,
400                     "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
401                     new String[] { path + "%", Integer.toString(path.length()), path });
402         }
403 
404         // Remove media store entry for this exact file.
405         final String path = file.getAbsolutePath();
406         resolver.delete(externalUri,
407                 "_data LIKE ?1 AND lower(_data)=lower(?2)",
408                 new String[] { path, path });
409     }
410 
411     @Override
queryDocument(String documentId, String[] projection)412     public Cursor queryDocument(String documentId, String[] projection)
413             throws FileNotFoundException {
414         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
415         includeFile(result, documentId, null);
416         return result;
417     }
418 
419     @Override
queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)420     public Cursor queryChildDocuments(
421             String parentDocumentId, String[] projection, String sortOrder)
422             throws FileNotFoundException {
423         final File parent = getFileForDocId(parentDocumentId);
424         final MatrixCursor result = new DirectoryCursor(
425                 resolveDocumentProjection(projection), parentDocumentId, parent);
426         for (File file : parent.listFiles()) {
427             includeFile(result, null, file);
428         }
429         return result;
430     }
431 
432     @Override
querySearchDocuments(String rootId, String query, String[] projection)433     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
434             throws FileNotFoundException {
435         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
436 
437         final File parent;
438         synchronized (mRootsLock) {
439             parent = mRoots.get(rootId).path;
440         }
441 
442         final LinkedList<File> pending = new LinkedList<File>();
443         pending.add(parent);
444         while (!pending.isEmpty() && result.getCount() < 24) {
445             final File file = pending.removeFirst();
446             if (file.isDirectory()) {
447                 for (File child : file.listFiles()) {
448                     pending.add(child);
449                 }
450             }
451             if (file.getName().toLowerCase().contains(query)) {
452                 includeFile(result, null, file);
453             }
454         }
455         return result;
456     }
457 
458     @Override
getDocumentType(String documentId)459     public String getDocumentType(String documentId) throws FileNotFoundException {
460         final File file = getFileForDocId(documentId);
461         return getTypeForFile(file);
462     }
463 
464     @Override
openDocument( String documentId, String mode, CancellationSignal signal)465     public ParcelFileDescriptor openDocument(
466             String documentId, String mode, CancellationSignal signal)
467             throws FileNotFoundException {
468         final File file = getFileForDocId(documentId);
469         final File visibleFile = getFileForDocId(documentId, true);
470 
471         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
472         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
473             return ParcelFileDescriptor.open(file, pfdMode);
474         } else {
475             try {
476                 // When finished writing, kick off media scanner
477                 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
478                     @Override
479                     public void onClose(IOException e) {
480                         final Intent intent = new Intent(
481                                 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
482                         intent.setData(Uri.fromFile(visibleFile));
483                         getContext().sendBroadcast(intent);
484                     }
485                 });
486             } catch (IOException e) {
487                 throw new FileNotFoundException("Failed to open for writing: " + e);
488             }
489         }
490     }
491 
492     @Override
493     public AssetFileDescriptor openDocumentThumbnail(
494             String documentId, Point sizeHint, CancellationSignal signal)
495             throws FileNotFoundException {
496         final File file = getFileForDocId(documentId);
497         return DocumentsContract.openImageThumbnail(file);
498     }
499 
500     @Override
501     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
502         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
503         synchronized (mRootsLock) {
504             for (int i = 0; i < mRoots.size(); i++) {
505                 final RootInfo root = mRoots.valueAt(i);
506                 pw.println("Root{" + root.rootId + "}:");
507                 pw.increaseIndent();
508                 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
509                 pw.println();
510                 pw.printPair("title", root.title);
511                 pw.printPair("docId", root.docId);
512                 pw.println();
513                 pw.printPair("path", root.path);
514                 pw.printPair("visiblePath", root.visiblePath);
515                 pw.decreaseIndent();
516                 pw.println();
517             }
518         }
519     }
520 
521     private static String getTypeForFile(File file) {
522         if (file.isDirectory()) {
523             return Document.MIME_TYPE_DIR;
524         } else {
525             return getTypeForName(file.getName());
526         }
527     }
528 
529     private static String getTypeForName(String name) {
530         final int lastDot = name.lastIndexOf('.');
531         if (lastDot >= 0) {
532             final String extension = name.substring(lastDot + 1).toLowerCase();
533             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
534             if (mime != null) {
535                 return mime;
536             }
537         }
538 
539         return "application/octet-stream";
540     }
541 
542     private void startObserving(File file, Uri notifyUri) {
543         synchronized (mObservers) {
544             DirectoryObserver observer = mObservers.get(file);
545             if (observer == null) {
546                 observer = new DirectoryObserver(
547                         file, getContext().getContentResolver(), notifyUri);
548                 observer.startWatching();
549                 mObservers.put(file, observer);
550             }
551             observer.mRefCount++;
552 
553             if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
554         }
555     }
556 
557     private void stopObserving(File file) {
558         synchronized (mObservers) {
559             DirectoryObserver observer = mObservers.get(file);
560             if (observer == null) return;
561 
562             observer.mRefCount--;
563             if (observer.mRefCount == 0) {
564                 mObservers.remove(file);
565                 observer.stopWatching();
566             }
567 
568             if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
569         }
570     }
571 
572     private static class DirectoryObserver extends FileObserver {
573         private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
574                 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
575 
576         private final File mFile;
577         private final ContentResolver mResolver;
578         private final Uri mNotifyUri;
579 
580         private int mRefCount = 0;
581 
582         public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
583             super(file.getAbsolutePath(), NOTIFY_EVENTS);
584             mFile = file;
585             mResolver = resolver;
586             mNotifyUri = notifyUri;
587         }
588 
589         @Override
590         public void onEvent(int event, String path) {
591             if ((event & NOTIFY_EVENTS) != 0) {
592                 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
593                 mResolver.notifyChange(mNotifyUri, null, false);
594             }
595         }
596 
597         @Override
598         public String toString() {
599             return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
600         }
601     }
602 
603     private class DirectoryCursor extends MatrixCursor {
604         private final File mFile;
605 
606         public DirectoryCursor(String[] columnNames, String docId, File file) {
607             super(columnNames);
608 
609             final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
610                     AUTHORITY, docId);
611             setNotificationUri(getContext().getContentResolver(), notifyUri);
612 
613             mFile = file;
614             startObserving(mFile, notifyUri);
615         }
616 
617         @Override
618         public void close() {
619             super.close();
620             stopObserving(mFile);
621         }
622     }
623 }
624