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