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