Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2015 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 android.support.provider;
     18 
     19 import android.content.res.AssetFileDescriptor;
     20 import android.content.res.Configuration;
     21 import android.database.ContentObserver;
     22 import android.database.Cursor;
     23 import android.database.MatrixCursor;
     24 import android.graphics.Point;
     25 import android.net.Uri;
     26 import android.os.CancellationSignal;
     27 import android.os.ParcelFileDescriptor;
     28 import android.provider.DocumentsContract.Document;
     29 import android.provider.DocumentsProvider;
     30 import android.support.annotation.Nullable;
     31 import android.util.Log;
     32 import android.util.LruCache;
     33 
     34 import java.io.Closeable;
     35 import java.io.File;
     36 import java.io.FileNotFoundException;
     37 import java.io.IOException;
     38 import java.util.HashMap;
     39 import java.util.Map;
     40 import java.util.concurrent.Callable;
     41 import java.util.concurrent.locks.Lock;
     42 import java.util.concurrent.locks.ReadWriteLock;
     43 import java.util.concurrent.locks.ReentrantReadWriteLock;
     44 
     45 /**
     46  * Provides basic implementation for creating, extracting and accessing
     47  * files within archives exposed by a document provider.
     48  *
     49  * <p>This class is thread safe. All methods can be called on any thread without
     50  * synchronization.
     51  *
     52  * TODO: Update the documentation. b/26047732
     53  * @hide
     54  */
     55 public class DocumentArchiveHelper implements Closeable {
     56     /**
     57      * Cursor column to be used for passing the local file path for documents.
     58      * If it's not specified, then a snapshot will be created, which is slower
     59      * and consumes more resources.
     60      *
     61      * <p>Type: STRING
     62      */
     63     public static final String COLUMN_LOCAL_FILE_PATH = "local_file_path";
     64 
     65     private static final String TAG = "DocumentArchiveHelper";
     66     private static final int OPENED_ARCHIVES_CACHE_SIZE = 4;
     67     private static final String[] ZIP_MIME_TYPES = {
     68             "application/zip", "application/x-zip", "application/x-zip-compressed"
     69     };
     70 
     71     private final DocumentsProvider mProvider;
     72     private final char mIdDelimiter;
     73 
     74     // @GuardedBy("mArchives")
     75     private final LruCache<String, Loader> mArchives =
     76             new LruCache<String, Loader>(OPENED_ARCHIVES_CACHE_SIZE) {
     77                 @Override
     78                 public void entryRemoved(boolean evicted, String key,
     79                         Loader oldValue, Loader newValue) {
     80                     oldValue.getWriteLock().lock();
     81                     try {
     82                         oldValue.get().close();
     83                     } catch (FileNotFoundException e) {
     84                         Log.e(TAG, "Failed to close an archive as it no longer exists.");
     85                     } finally {
     86                         oldValue.getWriteLock().unlock();
     87                     }
     88                 }
     89             };
     90 
     91     /**
     92      * Creates a helper for handling archived documents.
     93      *
     94      * @param provider Instance of a documents provider which provides archived documents.
     95      * @param idDelimiter A character used to create document IDs within archives. Can be any
     96      *            character which is not used in any other document ID. If your provider uses
     97      *            numbers as document IDs, the delimiter can be eg. a colon. However if your
     98      *            provider uses paths, then a delimiter can be any character not allowed in the
     99      *            path, which is often \0.
    100      */
    101     public DocumentArchiveHelper(DocumentsProvider provider, char idDelimiter) {
    102         mProvider = provider;
    103         mIdDelimiter = idDelimiter;
    104     }
    105 
    106     /**
    107      * Lists child documents of an archive or a directory within an
    108      * archive. Must be called only for archives with supported mime type,
    109      * or for documents within archives.
    110      *
    111      * @see DocumentsProvider.queryChildDocuments(String, String[], String)
    112      */
    113     public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
    114             @Nullable String sortOrder)
    115             throws FileNotFoundException {
    116         Loader loader = null;
    117         try {
    118             loader = obtainInstance(documentId);
    119             return loader.get().queryChildDocuments(documentId, projection, sortOrder);
    120         } finally {
    121             releaseInstance(loader);
    122         }
    123     }
    124 
    125     /**
    126      * Returns a MIME type of a document within an archive.
    127      *
    128      * @see DocumentsProvider.getDocumentType(String)
    129      */
    130     public String getDocumentType(String documentId) throws FileNotFoundException {
    131         Loader loader = null;
    132         try {
    133             loader = obtainInstance(documentId);
    134             return loader.get().getDocumentType(documentId);
    135         } finally {
    136             releaseInstance(loader);
    137         }
    138     }
    139 
    140     /**
    141      * Returns true if a document within an archive is a child or any descendant of the archive
    142      * document or another document within the archive.
    143      *
    144      * @see DocumentsProvider.isChildDocument(String, String)
    145      */
    146     public boolean isChildDocument(String parentDocumentId, String documentId) {
    147         Loader loader = null;
    148         try {
    149             loader = obtainInstance(documentId);
    150             return loader.get().isChildDocument(parentDocumentId, documentId);
    151         } catch (FileNotFoundException e) {
    152             throw new IllegalStateException(e);
    153         } finally {
    154             releaseInstance(loader);
    155         }
    156     }
    157 
    158     /**
    159      * Returns metadata of a document within an archive.
    160      *
    161      * @see DocumentsProvider.queryDocument(String, String[])
    162      */
    163     public Cursor queryDocument(String documentId, @Nullable String[] projection)
    164             throws FileNotFoundException {
    165         Loader loader = null;
    166         try {
    167             loader = obtainInstance(documentId);
    168             return loader.get().queryDocument(documentId, projection);
    169         } finally {
    170             releaseInstance(loader);
    171         }
    172     }
    173 
    174     /**
    175      * Opens a file within an archive.
    176      *
    177      * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
    178      */
    179     public ParcelFileDescriptor openDocument(
    180             String documentId, String mode, final CancellationSignal signal)
    181             throws FileNotFoundException {
    182         Loader loader = null;
    183         try {
    184             loader = obtainInstance(documentId);
    185             return loader.get().openDocument(documentId, mode, signal);
    186         } finally {
    187             releaseInstance(loader);
    188         }
    189     }
    190 
    191     /**
    192      * Opens a thumbnail of a file within an archive.
    193      *
    194      * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
    195      */
    196     public AssetFileDescriptor openDocumentThumbnail(
    197             String documentId, Point sizeHint, final CancellationSignal signal)
    198             throws FileNotFoundException {
    199         Loader loader = null;
    200         try {
    201             loader = obtainInstance(documentId);
    202             return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
    203         } finally {
    204             releaseInstance(loader);
    205         }
    206     }
    207 
    208     /**
    209      * Returns true if the passed document ID is for a document within an archive.
    210      */
    211     public boolean isArchivedDocument(String documentId) {
    212         return ParsedDocumentId.hasPath(documentId, mIdDelimiter);
    213     }
    214 
    215     /**
    216      * Returns true if the passed mime type is supported by the helper.
    217      */
    218     public boolean isSupportedArchiveType(String mimeType) {
    219         for (final String zipMimeType : ZIP_MIME_TYPES) {
    220             if (zipMimeType.equals(mimeType)) {
    221                 return true;
    222             }
    223         }
    224         return false;
    225     }
    226 
    227     /**
    228      * Closes the helper and disposes all existing archives. It will block until all ongoing
    229      * operations on each opened archive are finished.
    230      */
    231     @Override
    232     public void close() {
    233         synchronized (mArchives) {
    234             mArchives.evictAll();
    235         }
    236     }
    237 
    238     /**
    239      * Releases resources for an archive with the specified document ID. It will block until all
    240      * operations on the archive are finished. If not opened, the method does nothing.
    241      *
    242      * <p>Calling this method is optional. The helper automatically closes the least recently used
    243      * archives if too many archives are opened.
    244      *
    245      * @param archiveDocumentId ID of the archive file.
    246      */
    247     public void closeArchive(String documentId) {
    248         synchronized (mArchives) {
    249             mArchives.remove(documentId);
    250         }
    251     }
    252 
    253     private Loader obtainInstance(String documentId) throws FileNotFoundException {
    254         Loader loader;
    255         synchronized (mArchives) {
    256             loader = getInstanceUncheckedLocked(documentId);
    257             loader.getReadLock().lock();
    258         }
    259         return loader;
    260     }
    261 
    262     private void releaseInstance(@Nullable Loader loader) {
    263         if (loader != null) {
    264             loader.getReadLock().unlock();
    265         }
    266     }
    267 
    268     private Loader getInstanceUncheckedLocked(String documentId)
    269             throws FileNotFoundException {
    270         try {
    271             final ParsedDocumentId id = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter);
    272             if (mArchives.get(id.mArchiveId) != null) {
    273                 return mArchives.get(id.mArchiveId);
    274             }
    275 
    276             final Cursor cursor = mProvider.queryDocument(id.mArchiveId, new String[]
    277                     { Document.COLUMN_MIME_TYPE, COLUMN_LOCAL_FILE_PATH });
    278             cursor.moveToFirst();
    279             final String mimeType = cursor.getString(cursor.getColumnIndex(
    280                     Document.COLUMN_MIME_TYPE));
    281             Preconditions.checkArgument(isSupportedArchiveType(mimeType),
    282                     "Unsupported archive type.");
    283             final int columnIndex = cursor.getColumnIndex(COLUMN_LOCAL_FILE_PATH);
    284             final String localFilePath = columnIndex != -1 ? cursor.getString(columnIndex) : null;
    285             final File localFile = localFilePath != null ? new File(localFilePath) : null;
    286             final Uri notificationUri = cursor.getNotificationUri();
    287             final Loader loader = new Loader(mProvider, localFile, id, mIdDelimiter,
    288                     notificationUri);
    289 
    290             // Remove the instance from mArchives collection once the archive file changes.
    291             if (notificationUri != null) {
    292                 final LruCache<String, Loader> finalArchives = mArchives;
    293                 mProvider.getContext().getContentResolver().registerContentObserver(notificationUri,
    294                         false,
    295                         new ContentObserver(null) {
    296                             @Override
    297                             public void onChange(boolean selfChange, Uri uri) {
    298                                 synchronized (mArchives) {
    299                                     final Loader currentLoader = mArchives.get(id.mArchiveId);
    300                                     if (currentLoader == loader) {
    301                                         mArchives.remove(id.mArchiveId);
    302                                     }
    303                                 }
    304                             }
    305                         });
    306             }
    307 
    308             mArchives.put(id.mArchiveId, loader);
    309             return loader;
    310         } catch (IOException e) {
    311             // DocumentsProvider doesn't use IOException. For consistency convert it to
    312             // IllegalStateException.
    313             throw new IllegalStateException(e);
    314         }
    315     }
    316 
    317     /**
    318      * Loads an instance of DocumentArchive lazily.
    319      */
    320     private static final class Loader {
    321         private final DocumentsProvider mProvider;
    322         private final File mLocalFile;
    323         private final ParsedDocumentId mId;
    324         private final char mIdDelimiter;
    325         private final Uri mNotificationUri;
    326         private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
    327         private DocumentArchive mArchive = null;
    328 
    329         Loader(DocumentsProvider provider, @Nullable File localFile, ParsedDocumentId id,
    330                 char idDelimiter, Uri notificationUri) {
    331             this.mProvider = provider;
    332             this.mLocalFile = localFile;
    333             this.mId = id;
    334             this.mIdDelimiter = idDelimiter;
    335             this.mNotificationUri = notificationUri;
    336         }
    337 
    338         synchronized DocumentArchive get() throws FileNotFoundException {
    339             if (mArchive != null) {
    340                 return mArchive;
    341             }
    342 
    343             try {
    344                 if (mLocalFile != null) {
    345                     mArchive = DocumentArchive.createForLocalFile(
    346                             mProvider.getContext(), mLocalFile, mId.mArchiveId, mIdDelimiter,
    347                             mNotificationUri);
    348                 } else {
    349                     mArchive = DocumentArchive.createForParcelFileDescriptor(
    350                             mProvider.getContext(),
    351                             mProvider.openDocument(mId.mArchiveId, "r", null /* signal */),
    352                             mId.mArchiveId, mIdDelimiter, mNotificationUri);
    353                 }
    354             } catch (IOException e) {
    355                 throw new IllegalStateException(e);
    356             }
    357 
    358             return mArchive;
    359         }
    360 
    361         Lock getReadLock() {
    362             return mLock.readLock();
    363         }
    364 
    365         Lock getWriteLock() {
    366             return mLock.writeLock();
    367         }
    368     }
    369 }
    370