Home | History | Annotate | Download | only in externalstorage
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.externalstorage;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.content.res.AssetFileDescriptor;
     22 import android.database.Cursor;
     23 import android.database.MatrixCursor;
     24 import android.database.MatrixCursor.RowBuilder;
     25 import android.graphics.Point;
     26 import android.net.Uri;
     27 import android.os.CancellationSignal;
     28 import android.os.Environment;
     29 import android.os.FileObserver;
     30 import android.os.ParcelFileDescriptor;
     31 import android.os.storage.StorageManager;
     32 import android.os.storage.StorageVolume;
     33 import android.provider.DocumentsContract;
     34 import android.provider.DocumentsContract.Document;
     35 import android.provider.DocumentsContract.Root;
     36 import android.provider.DocumentsProvider;
     37 import android.util.Log;
     38 import android.webkit.MimeTypeMap;
     39 
     40 import com.android.internal.annotations.GuardedBy;
     41 import com.google.android.collect.Lists;
     42 import com.google.android.collect.Maps;
     43 
     44 import java.io.File;
     45 import java.io.FileNotFoundException;
     46 import java.io.IOException;
     47 import java.util.ArrayList;
     48 import java.util.HashMap;
     49 import java.util.LinkedList;
     50 import java.util.Map;
     51 
     52 public class ExternalStorageProvider extends DocumentsProvider {
     53     private static final String TAG = "ExternalStorage";
     54 
     55     private static final boolean LOG_INOTIFY = false;
     56 
     57     public static final String AUTHORITY = "com.android.externalstorage.documents";
     58 
     59     // docId format: root:path/to/file
     60 
     61     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     62             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
     63             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
     64     };
     65 
     66     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
     67             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
     68             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     69     };
     70 
     71     private static class RootInfo {
     72         public String rootId;
     73         public int flags;
     74         public String title;
     75         public String docId;
     76     }
     77 
     78     private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
     79 
     80     private StorageManager mStorageManager;
     81 
     82     private final Object mRootsLock = new Object();
     83 
     84     @GuardedBy("mRootsLock")
     85     private ArrayList<RootInfo> mRoots;
     86     @GuardedBy("mRootsLock")
     87     private HashMap<String, RootInfo> mIdToRoot;
     88     @GuardedBy("mRootsLock")
     89     private HashMap<String, File> mIdToPath;
     90 
     91     @GuardedBy("mObservers")
     92     private Map<File, DirectoryObserver> mObservers = Maps.newHashMap();
     93 
     94     @Override
     95     public boolean onCreate() {
     96         mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
     97 
     98         mRoots = Lists.newArrayList();
     99         mIdToRoot = Maps.newHashMap();
    100         mIdToPath = Maps.newHashMap();
    101 
    102         updateVolumes();
    103 
    104         return true;
    105     }
    106 
    107     public void updateVolumes() {
    108         synchronized (mRootsLock) {
    109             updateVolumesLocked();
    110         }
    111     }
    112 
    113     private void updateVolumesLocked() {
    114         mRoots.clear();
    115         mIdToPath.clear();
    116         mIdToRoot.clear();
    117 
    118         final StorageVolume[] volumes = mStorageManager.getVolumeList();
    119         for (StorageVolume volume : volumes) {
    120             final boolean mounted = Environment.MEDIA_MOUNTED.equals(volume.getState())
    121                     || Environment.MEDIA_MOUNTED_READ_ONLY.equals(volume.getState());
    122             if (!mounted) continue;
    123 
    124             final String rootId;
    125             if (volume.isPrimary() && volume.isEmulated()) {
    126                 rootId = ROOT_ID_PRIMARY_EMULATED;
    127             } else if (volume.getUuid() != null) {
    128                 rootId = volume.getUuid();
    129             } else {
    130                 Log.d(TAG, "Missing UUID for " + volume.getPath() + "; skipping");
    131                 continue;
    132             }
    133 
    134             if (mIdToPath.containsKey(rootId)) {
    135                 Log.w(TAG, "Duplicate UUID " + rootId + "; skipping");
    136                 continue;
    137             }
    138 
    139             try {
    140                 final File path = volume.getPathFile();
    141                 mIdToPath.put(rootId, path);
    142 
    143                 final RootInfo root = new RootInfo();
    144                 root.rootId = rootId;
    145                 root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
    146                         | Root.FLAG_SUPPORTS_SEARCH;
    147                 if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
    148                     root.title = getContext().getString(R.string.root_internal_storage);
    149                 } else {
    150                     root.title = volume.getUserLabel();
    151                 }
    152                 root.docId = getDocIdForFile(path);
    153                 mRoots.add(root);
    154                 mIdToRoot.put(rootId, root);
    155             } catch (FileNotFoundException e) {
    156                 throw new IllegalStateException(e);
    157             }
    158         }
    159 
    160         Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
    161 
    162         getContext().getContentResolver()
    163                 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
    164     }
    165 
    166     private static String[] resolveRootProjection(String[] projection) {
    167         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
    168     }
    169 
    170     private static String[] resolveDocumentProjection(String[] projection) {
    171         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
    172     }
    173 
    174     private String getDocIdForFile(File file) throws FileNotFoundException {
    175         String path = file.getAbsolutePath();
    176 
    177         // Find the most-specific root path
    178         Map.Entry<String, File> mostSpecific = null;
    179         synchronized (mRootsLock) {
    180             for (Map.Entry<String, File> root : mIdToPath.entrySet()) {
    181                 final String rootPath = root.getValue().getPath();
    182                 if (path.startsWith(rootPath) && (mostSpecific == null
    183                         || rootPath.length() > mostSpecific.getValue().getPath().length())) {
    184                     mostSpecific = root;
    185                 }
    186             }
    187         }
    188 
    189         if (mostSpecific == null) {
    190             throw new FileNotFoundException("Failed to find root that contains " + path);
    191         }
    192 
    193         // Start at first char of path under root
    194         final String rootPath = mostSpecific.getValue().getPath();
    195         if (rootPath.equals(path)) {
    196             path = "";
    197         } else if (rootPath.endsWith("/")) {
    198             path = path.substring(rootPath.length());
    199         } else {
    200             path = path.substring(rootPath.length() + 1);
    201         }
    202 
    203         return mostSpecific.getKey() + ':' + path;
    204     }
    205 
    206     private File getFileForDocId(String docId) throws FileNotFoundException {
    207         final int splitIndex = docId.indexOf(':', 1);
    208         final String tag = docId.substring(0, splitIndex);
    209         final String path = docId.substring(splitIndex + 1);
    210 
    211         File target;
    212         synchronized (mRootsLock) {
    213             target = mIdToPath.get(tag);
    214         }
    215         if (target == null) {
    216             throw new FileNotFoundException("No root for " + tag);
    217         }
    218         if (!target.exists()) {
    219             target.mkdirs();
    220         }
    221         target = new File(target, path);
    222         if (!target.exists()) {
    223             throw new FileNotFoundException("Missing file for " + docId + " at " + target);
    224         }
    225         return target;
    226     }
    227 
    228     private void includeFile(MatrixCursor result, String docId, File file)
    229             throws FileNotFoundException {
    230         if (docId == null) {
    231             docId = getDocIdForFile(file);
    232         } else {
    233             file = getFileForDocId(docId);
    234         }
    235 
    236         int flags = 0;
    237 
    238         if (file.canWrite()) {
    239             if (file.isDirectory()) {
    240                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
    241             } else {
    242                 flags |= Document.FLAG_SUPPORTS_WRITE;
    243             }
    244             flags |= Document.FLAG_SUPPORTS_DELETE;
    245         }
    246 
    247         final String displayName = file.getName();
    248         final String mimeType = getTypeForFile(file);
    249         if (mimeType.startsWith("image/")) {
    250             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
    251         }
    252 
    253         final RowBuilder row = result.newRow();
    254         row.add(Document.COLUMN_DOCUMENT_ID, docId);
    255         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
    256         row.add(Document.COLUMN_SIZE, file.length());
    257         row.add(Document.COLUMN_MIME_TYPE, mimeType);
    258         row.add(Document.COLUMN_FLAGS, flags);
    259 
    260         // Only publish dates reasonably after epoch
    261         long lastModified = file.lastModified();
    262         if (lastModified > 31536000000L) {
    263             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
    264         }
    265     }
    266 
    267     @Override
    268     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
    269         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
    270         synchronized (mRootsLock) {
    271             for (String rootId : mIdToPath.keySet()) {
    272                 final RootInfo root = mIdToRoot.get(rootId);
    273                 final File path = mIdToPath.get(rootId);
    274 
    275                 final RowBuilder row = result.newRow();
    276                 row.add(Root.COLUMN_ROOT_ID, root.rootId);
    277                 row.add(Root.COLUMN_FLAGS, root.flags);
    278                 row.add(Root.COLUMN_TITLE, root.title);
    279                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
    280                 row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace());
    281             }
    282         }
    283         return result;
    284     }
    285 
    286     @Override
    287     public String createDocument(String docId, String mimeType, String displayName)
    288             throws FileNotFoundException {
    289         final File parent = getFileForDocId(docId);
    290         File file;
    291 
    292         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    293             file = new File(parent, displayName);
    294             if (!file.mkdir()) {
    295                 throw new IllegalStateException("Failed to mkdir " + file);
    296             }
    297         } else {
    298             displayName = removeExtension(mimeType, displayName);
    299             file = new File(parent, addExtension(mimeType, displayName));
    300 
    301             // If conflicting file, try adding counter suffix
    302             int n = 0;
    303             while (file.exists() && n++ < 32) {
    304                 file = new File(parent, addExtension(mimeType, displayName + " (" + n + ")"));
    305             }
    306 
    307             try {
    308                 if (!file.createNewFile()) {
    309                     throw new IllegalStateException("Failed to touch " + file);
    310                 }
    311             } catch (IOException e) {
    312                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
    313             }
    314         }
    315         return getDocIdForFile(file);
    316     }
    317 
    318     @Override
    319     public void deleteDocument(String docId) throws FileNotFoundException {
    320         final File file = getFileForDocId(docId);
    321         if (!file.delete()) {
    322             throw new IllegalStateException("Failed to delete " + file);
    323         }
    324     }
    325 
    326     @Override
    327     public Cursor queryDocument(String documentId, String[] projection)
    328             throws FileNotFoundException {
    329         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    330         includeFile(result, documentId, null);
    331         return result;
    332     }
    333 
    334     @Override
    335     public Cursor queryChildDocuments(
    336             String parentDocumentId, String[] projection, String sortOrder)
    337             throws FileNotFoundException {
    338         final File parent = getFileForDocId(parentDocumentId);
    339         final MatrixCursor result = new DirectoryCursor(
    340                 resolveDocumentProjection(projection), parentDocumentId, parent);
    341         for (File file : parent.listFiles()) {
    342             includeFile(result, null, file);
    343         }
    344         return result;
    345     }
    346 
    347     @Override
    348     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
    349             throws FileNotFoundException {
    350         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    351 
    352         final File parent;
    353         synchronized (mRootsLock) {
    354             parent = mIdToPath.get(rootId);
    355         }
    356 
    357         final LinkedList<File> pending = new LinkedList<File>();
    358         pending.add(parent);
    359         while (!pending.isEmpty() && result.getCount() < 24) {
    360             final File file = pending.removeFirst();
    361             if (file.isDirectory()) {
    362                 for (File child : file.listFiles()) {
    363                     pending.add(child);
    364                 }
    365             }
    366             if (file.getName().toLowerCase().contains(query)) {
    367                 includeFile(result, null, file);
    368             }
    369         }
    370         return result;
    371     }
    372 
    373     @Override
    374     public String getDocumentType(String documentId) throws FileNotFoundException {
    375         final File file = getFileForDocId(documentId);
    376         return getTypeForFile(file);
    377     }
    378 
    379     @Override
    380     public ParcelFileDescriptor openDocument(
    381             String documentId, String mode, CancellationSignal signal)
    382             throws FileNotFoundException {
    383         final File file = getFileForDocId(documentId);
    384         return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
    385     }
    386 
    387     @Override
    388     public AssetFileDescriptor openDocumentThumbnail(
    389             String documentId, Point sizeHint, CancellationSignal signal)
    390             throws FileNotFoundException {
    391         final File file = getFileForDocId(documentId);
    392         return DocumentsContract.openImageThumbnail(file);
    393     }
    394 
    395     private static String getTypeForFile(File file) {
    396         if (file.isDirectory()) {
    397             return Document.MIME_TYPE_DIR;
    398         } else {
    399             return getTypeForName(file.getName());
    400         }
    401     }
    402 
    403     private static String getTypeForName(String name) {
    404         final int lastDot = name.lastIndexOf('.');
    405         if (lastDot >= 0) {
    406             final String extension = name.substring(lastDot + 1).toLowerCase();
    407             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    408             if (mime != null) {
    409                 return mime;
    410             }
    411         }
    412 
    413         return "application/octet-stream";
    414     }
    415 
    416     /**
    417      * Remove file extension from name, but only if exact MIME type mapping
    418      * exists. This means we can reapply the extension later.
    419      */
    420     private static String removeExtension(String mimeType, String name) {
    421         final int lastDot = name.lastIndexOf('.');
    422         if (lastDot >= 0) {
    423             final String extension = name.substring(lastDot + 1).toLowerCase();
    424             final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    425             if (mimeType.equals(nameMime)) {
    426                 return name.substring(0, lastDot);
    427             }
    428         }
    429         return name;
    430     }
    431 
    432     /**
    433      * Add file extension to name, but only if exact MIME type mapping exists.
    434      */
    435     private static String addExtension(String mimeType, String name) {
    436         final String extension = MimeTypeMap.getSingleton()
    437                 .getExtensionFromMimeType(mimeType);
    438         if (extension != null) {
    439             return name + "." + extension;
    440         }
    441         return name;
    442     }
    443 
    444     private void startObserving(File file, Uri notifyUri) {
    445         synchronized (mObservers) {
    446             DirectoryObserver observer = mObservers.get(file);
    447             if (observer == null) {
    448                 observer = new DirectoryObserver(
    449                         file, getContext().getContentResolver(), notifyUri);
    450                 observer.startWatching();
    451                 mObservers.put(file, observer);
    452             }
    453             observer.mRefCount++;
    454 
    455             if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
    456         }
    457     }
    458 
    459     private void stopObserving(File file) {
    460         synchronized (mObservers) {
    461             DirectoryObserver observer = mObservers.get(file);
    462             if (observer == null) return;
    463 
    464             observer.mRefCount--;
    465             if (observer.mRefCount == 0) {
    466                 mObservers.remove(file);
    467                 observer.stopWatching();
    468             }
    469 
    470             if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
    471         }
    472     }
    473 
    474     private static class DirectoryObserver extends FileObserver {
    475         private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
    476                 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
    477 
    478         private final File mFile;
    479         private final ContentResolver mResolver;
    480         private final Uri mNotifyUri;
    481 
    482         private int mRefCount = 0;
    483 
    484         public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
    485             super(file.getAbsolutePath(), NOTIFY_EVENTS);
    486             mFile = file;
    487             mResolver = resolver;
    488             mNotifyUri = notifyUri;
    489         }
    490 
    491         @Override
    492         public void onEvent(int event, String path) {
    493             if ((event & NOTIFY_EVENTS) != 0) {
    494                 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
    495                 mResolver.notifyChange(mNotifyUri, null, false);
    496             }
    497         }
    498 
    499         @Override
    500         public String toString() {
    501             return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
    502         }
    503     }
    504 
    505     private class DirectoryCursor extends MatrixCursor {
    506         private final File mFile;
    507 
    508         public DirectoryCursor(String[] columnNames, String docId, File file) {
    509             super(columnNames);
    510 
    511             final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
    512                     AUTHORITY, docId);
    513             setNotificationUri(getContext().getContentResolver(), notifyUri);
    514 
    515             mFile = file;
    516             startObserving(mFile, notifyUri);
    517         }
    518 
    519         @Override
    520         public void close() {
    521             super.close();
    522             stopObserving(mFile);
    523         }
    524     }
    525 }
    526