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