Home | History | Annotate | Download | only in archives
      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 com.android.documentsui.archives;
     18 
     19 import android.content.ContentProviderClient;
     20 import android.content.res.AssetFileDescriptor;
     21 import android.database.Cursor;
     22 import android.database.MatrixCursor;
     23 import android.database.MatrixCursor.RowBuilder;
     24 import android.graphics.Point;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.os.CancellationSignal;
     28 import android.os.ParcelFileDescriptor;
     29 import android.provider.DocumentsContract;
     30 import android.provider.DocumentsContract.Document;
     31 import android.provider.DocumentsContract.Root;
     32 import android.provider.DocumentsProvider;
     33 import android.provider.MetadataReader;
     34 import android.support.annotation.Nullable;
     35 import android.util.Log;
     36 
     37 import com.android.documentsui.R;
     38 import com.android.internal.annotations.GuardedBy;
     39 
     40 import libcore.io.IoUtils;
     41 
     42 import java.io.FileNotFoundException;
     43 import java.io.IOException;
     44 import java.io.InputStream;
     45 import java.util.HashMap;
     46 import java.util.Map;
     47 import java.util.Objects;
     48 
     49 /**
     50  * Provides basic implementation for creating, extracting and accessing
     51  * files within archives exposed by a document provider.
     52  *
     53  * <p>This class is thread safe. All methods can be called on any thread without
     54  * synchronization.
     55  */
     56 public class ArchivesProvider extends DocumentsProvider {
     57     public static final String AUTHORITY = "com.android.documentsui.archives";
     58 
     59     private static final String[] DEFAULT_ROOTS_PROJECTION = new String[] {
     60             Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, Root.COLUMN_FLAGS,
     61             Root.COLUMN_ICON };
     62     private static final String TAG = "ArchivesProvider";
     63     private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive";
     64     private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive";
     65     private static final String[] ZIP_MIME_TYPES = {
     66             "application/zip", "application/x-zip", "application/x-zip-compressed"
     67     };
     68 
     69     @GuardedBy("mArchives")
     70     private final Map<Key, Loader> mArchives = new HashMap<>();
     71 
     72     @Override
     73     public Bundle call(String method, String arg, Bundle extras) {
     74         if (METHOD_ACQUIRE_ARCHIVE.equals(method)) {
     75             acquireArchive(arg);
     76             return null;
     77         }
     78 
     79         if (METHOD_RELEASE_ARCHIVE.equals(method)) {
     80             releaseArchive(arg);
     81             return null;
     82         }
     83 
     84         return super.call(method, arg, extras);
     85     }
     86 
     87     @Override
     88     public boolean onCreate() {
     89         return true;
     90     }
     91 
     92     @Override
     93     public Cursor queryRoots(String[] projection) {
     94         // No roots provided.
     95         return new MatrixCursor(projection != null ? projection : DEFAULT_ROOTS_PROJECTION);
     96     }
     97 
     98     @Override
     99     public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
    100             @Nullable String sortOrder)
    101             throws FileNotFoundException {
    102         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
    103         final Loader loader = getLoaderOrThrow(documentId);
    104         final int status = loader.getStatus();
    105         // If already loaded, then forward the request to the archive.
    106         if (status == Loader.STATUS_OPENED) {
    107             return loader.get().queryChildDocuments(documentId, projection, sortOrder);
    108         }
    109 
    110         final MatrixCursor cursor = new MatrixCursor(
    111                 projection != null ? projection : Archive.DEFAULT_PROJECTION);
    112         final Bundle bundle = new Bundle();
    113 
    114         switch (status) {
    115             case Loader.STATUS_OPENING:
    116                 bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
    117                 break;
    118 
    119             case Loader.STATUS_FAILED:
    120                 // Return an empty cursor with EXTRA_LOADING, which shows spinner
    121                 // in DocumentsUI. Once the archive is loaded, the notification will
    122                 // be sent, and the directory reloaded.
    123                 bundle.putString(DocumentsContract.EXTRA_ERROR,
    124                         getContext().getString(R.string.archive_loading_failed));
    125                 break;
    126         }
    127 
    128         cursor.setExtras(bundle);
    129         cursor.setNotificationUri(getContext().getContentResolver(),
    130                 buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
    131         return cursor;
    132     }
    133 
    134     @Override
    135     public String getDocumentType(String documentId) throws FileNotFoundException {
    136         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
    137         if (archiveId.mPath.equals("/")) {
    138             return Document.MIME_TYPE_DIR;
    139         }
    140 
    141         final Loader loader = getLoaderOrThrow(documentId);
    142         return loader.get().getDocumentType(documentId);
    143     }
    144 
    145     @Override
    146     public boolean isChildDocument(String parentDocumentId, String documentId) {
    147         final Loader loader = getLoaderOrThrow(documentId);
    148         return loader.get().isChildDocument(parentDocumentId, documentId);
    149     }
    150 
    151     @Override
    152     public @Nullable Bundle getDocumentMetadata(String documentId)
    153             throws FileNotFoundException {
    154 
    155         final Archive archive = getLoaderOrThrow(documentId).get();
    156         final String mimeType = archive.getDocumentType(documentId);
    157 
    158         if (!MetadataReader.isSupportedMimeType(mimeType)) {
    159             return null;
    160         }
    161 
    162         InputStream stream = null;
    163         try {
    164             stream = new ParcelFileDescriptor.AutoCloseInputStream(
    165                     openDocument(documentId, "r", null));
    166             final Bundle metadata = new Bundle();
    167             MetadataReader.getMetadata(metadata, stream, mimeType, null);
    168             return metadata;
    169         } catch (IOException e) {
    170             Log.e(TAG, "An error occurred retrieving the metadata.", e);
    171             return null;
    172         } finally {
    173             IoUtils.closeQuietly(stream);
    174         }
    175     }
    176 
    177     @Override
    178     public Cursor queryDocument(String documentId, @Nullable String[] projection)
    179             throws FileNotFoundException {
    180         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
    181         if (archiveId.mPath.equals("/")) {
    182             try (final Cursor archiveCursor = getContext().getContentResolver().query(
    183                     archiveId.mArchiveUri,
    184                     new String[] { Document.COLUMN_DISPLAY_NAME },
    185                     null, null, null, null)) {
    186                 if (archiveCursor == null || !archiveCursor.moveToFirst()) {
    187                     throw new FileNotFoundException(
    188                             "Cannot resolve display name of the archive.");
    189                 }
    190                 final String displayName = archiveCursor.getString(
    191                         archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
    192 
    193                 final MatrixCursor cursor = new MatrixCursor(
    194                         projection != null ? projection : Archive.DEFAULT_PROJECTION);
    195                 final RowBuilder row = cursor.newRow();
    196                 row.add(Document.COLUMN_DOCUMENT_ID, documentId);
    197                 row.add(Document.COLUMN_DISPLAY_NAME, displayName);
    198                 row.add(Document.COLUMN_SIZE, 0);
    199                 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
    200                 return cursor;
    201             }
    202         }
    203 
    204         final Loader loader = getLoaderOrThrow(documentId);
    205         return loader.get().queryDocument(documentId, projection);
    206     }
    207 
    208     @Override
    209     public String createDocument(
    210             String parentDocumentId, String mimeType, String displayName)
    211             throws FileNotFoundException {
    212         final Loader loader = getLoaderOrThrow(parentDocumentId);
    213         return loader.get().createDocument(parentDocumentId, mimeType, displayName);
    214     }
    215 
    216     @Override
    217     public ParcelFileDescriptor openDocument(
    218             String documentId, String mode, final CancellationSignal signal)
    219             throws FileNotFoundException {
    220         final Loader loader = getLoaderOrThrow(documentId);
    221         return loader.get().openDocument(documentId, mode, signal);
    222     }
    223 
    224     @Override
    225     public AssetFileDescriptor openDocumentThumbnail(
    226             String documentId, Point sizeHint, final CancellationSignal signal)
    227             throws FileNotFoundException {
    228         final Loader loader = getLoaderOrThrow(documentId);
    229         return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
    230     }
    231 
    232     /**
    233      * Returns true if the passed mime type is supported by the helper.
    234      */
    235     public static boolean isSupportedArchiveType(String mimeType) {
    236         for (final String zipMimeType : ZIP_MIME_TYPES) {
    237             if (zipMimeType.equals(mimeType)) {
    238                 return true;
    239             }
    240         }
    241         return false;
    242     }
    243 
    244     /**
    245      * Creates a Uri for accessing an archive with the specified access mode.
    246      *
    247      * @see ParcelFileDescriptor#MODE_READ
    248      * @see ParcelFileDescriptor#MODE_WRITE
    249      */
    250     public static Uri buildUriForArchive(Uri externalUri, int accessMode) {
    251         return DocumentsContract.buildDocumentUri(AUTHORITY,
    252                 new ArchiveId(externalUri, accessMode, "/").toDocumentId());
    253     }
    254 
    255     /**
    256      * Acquires an archive.
    257      */
    258     public static void acquireArchive(ContentProviderClient client, Uri archiveUri) {
    259         Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
    260                 "Mismatching authority. Expected: %s, actual: %s.");
    261         final String documentId = DocumentsContract.getDocumentId(archiveUri);
    262 
    263         try {
    264             client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null);
    265         } catch (Exception e) {
    266             Log.w(TAG, "Failed to acquire archive.", e);
    267         }
    268     }
    269 
    270     /**
    271      * Releases an archive.
    272      */
    273     public static void releaseArchive(ContentProviderClient client, Uri archiveUri) {
    274         Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
    275                 "Mismatching authority. Expected: %s, actual: %s.");
    276         final String documentId = DocumentsContract.getDocumentId(archiveUri);
    277 
    278         try {
    279             client.call(METHOD_RELEASE_ARCHIVE, documentId, null);
    280         } catch (Exception e) {
    281             Log.w(TAG, "Failed to release archive.", e);
    282         }
    283     }
    284 
    285     /**
    286      * The archive won't close until all clients release it.
    287      */
    288     private void acquireArchive(String documentId) {
    289         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
    290         synchronized (mArchives) {
    291             final Key key = Key.fromArchiveId(archiveId);
    292             Loader loader = mArchives.get(key);
    293             if (loader == null) {
    294                 // TODO: Pass parent Uri so the loader can acquire the parent's notification Uri.
    295                 loader = new Loader(getContext(), archiveId.mArchiveUri, archiveId.mAccessMode,
    296                         null);
    297                 mArchives.put(key, loader);
    298             }
    299             loader.acquire();
    300             mArchives.put(key, loader);
    301         }
    302     }
    303 
    304     /**
    305      * If all clients release the archive, then it will be closed.
    306      */
    307     private void releaseArchive(String documentId) {
    308         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
    309         final Key key = Key.fromArchiveId(archiveId);
    310         synchronized (mArchives) {
    311             final Loader loader = mArchives.get(key);
    312             loader.release();
    313             final int status = loader.getStatus();
    314             if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) {
    315                 mArchives.remove(key);
    316             }
    317         }
    318     }
    319 
    320     private Loader getLoaderOrThrow(String documentId) {
    321         final ArchiveId id = ArchiveId.fromDocumentId(documentId);
    322         final Key key = Key.fromArchiveId(id);
    323         synchronized (mArchives) {
    324             final Loader loader = mArchives.get(key);
    325             if (loader == null) {
    326                 throw new IllegalStateException("Archive not acquired.");
    327             }
    328             return loader;
    329         }
    330     }
    331 
    332     private static class Key {
    333         Uri archiveUri;
    334         int accessMode;
    335 
    336         public Key(Uri archiveUri, int accessMode) {
    337             this.archiveUri = archiveUri;
    338             this.accessMode = accessMode;
    339         }
    340 
    341         public static Key fromArchiveId(ArchiveId id) {
    342             return new Key(id.mArchiveUri, id.mAccessMode);
    343         }
    344 
    345         @Override
    346         public boolean equals(Object other) {
    347             if (other == null) {
    348                 return false;
    349             }
    350             if (!(other instanceof Key)) {
    351                 return false;
    352             }
    353             return archiveUri.equals(((Key) other).archiveUri) &&
    354                 accessMode == ((Key) other).accessMode;
    355         }
    356 
    357         @Override
    358         public int hashCode() {
    359             return Objects.hash(archiveUri, accessMode);
    360         }
    361     }
    362 }
    363