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.Context;
     20 import android.content.res.AssetFileDescriptor;
     21 import android.database.Cursor;
     22 import android.database.MatrixCursor;
     23 import android.graphics.Point;
     24 import android.net.Uri;
     25 import android.os.CancellationSignal;
     26 import android.os.ParcelFileDescriptor;
     27 import android.os.storage.StorageManager;
     28 import android.provider.DocumentsContract;
     29 import android.provider.MetadataReader;
     30 import android.provider.DocumentsContract.Document;
     31 import android.support.annotation.Nullable;
     32 import android.system.ErrnoException;
     33 import android.system.Os;
     34 import android.system.OsConstants;
     35 import android.text.TextUtils;
     36 import android.webkit.MimeTypeMap;
     37 
     38 import com.android.internal.annotations.GuardedBy;
     39 import com.android.internal.util.Preconditions;
     40 
     41 import java.io.Closeable;
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Locale;
     47 import java.util.Map;
     48 import java.util.concurrent.LinkedBlockingQueue;
     49 import java.util.zip.ZipEntry;
     50 
     51 /**
     52  * Provides basic implementation for creating, extracting and accessing
     53  * files within archives exposed by a document provider.
     54  *
     55  * <p>This class is thread safe.
     56  */
     57 public abstract class Archive implements Closeable {
     58     private static final String TAG = "Archive";
     59 
     60     public static final String[] DEFAULT_PROJECTION = new String[] {
     61             Document.COLUMN_DOCUMENT_ID,
     62             Document.COLUMN_DISPLAY_NAME,
     63             Document.COLUMN_MIME_TYPE,
     64             Document.COLUMN_SIZE,
     65             Document.COLUMN_FLAGS
     66     };
     67 
     68     final Context mContext;
     69     final Uri mArchiveUri;
     70     final int mAccessMode;
     71     final Uri mNotificationUri;
     72 
     73     // The container as well as values are guarded by mEntries.
     74     @GuardedBy("mEntries")
     75     final Map<String, ZipEntry> mEntries;
     76 
     77     // The container as well as values and elements of values are guarded by mEntries.
     78     @GuardedBy("mEntries")
     79     final Map<String, List<ZipEntry>> mTree;
     80 
     81     Archive(
     82             Context context,
     83             Uri archiveUri,
     84             int accessMode,
     85             @Nullable Uri notificationUri) {
     86         mContext = context;
     87         mArchiveUri = archiveUri;
     88         mAccessMode = accessMode;
     89         mNotificationUri = notificationUri;
     90 
     91         mTree = new HashMap<>();
     92         mEntries = new HashMap<>();
     93     }
     94 
     95     /**
     96      * Returns a valid, normalized path for an entry.
     97      */
     98     public static String getEntryPath(ZipEntry entry) {
     99         Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
    100                 "Ill-formated ZIP-file.");
    101         if (entry.getName().startsWith("/")) {
    102             return entry.getName();
    103         } else {
    104             return "/" + entry.getName();
    105         }
    106     }
    107 
    108     /**
    109      * Returns true if the file descriptor is seekable.
    110      * @param descriptor File descriptor to check.
    111      */
    112     public static boolean canSeek(ParcelFileDescriptor descriptor) {
    113         try {
    114             return Os.lseek(descriptor.getFileDescriptor(), 0,
    115                     OsConstants.SEEK_CUR) == 0;
    116         } catch (ErrnoException e) {
    117             return false;
    118         }
    119     }
    120 
    121     /**
    122      * Lists child documents of an archive or a directory within an
    123      * archive. Must be called only for archives with supported mime type,
    124      * or for documents within archives.
    125      *
    126      * @see DocumentsProvider.queryChildDocuments(String, String[], String)
    127      */
    128     public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
    129             @Nullable String sortOrder) throws FileNotFoundException {
    130         final ArchiveId parsedParentId = ArchiveId.fromDocumentId(documentId);
    131         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
    132                 "Mismatching archive Uri. Expected: %s, actual: %s.");
    133 
    134         final MatrixCursor result = new MatrixCursor(
    135                 projection != null ? projection : DEFAULT_PROJECTION);
    136         if (mNotificationUri != null) {
    137             result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
    138         }
    139 
    140         synchronized (mEntries) {
    141             final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath);
    142             if (parentList == null) {
    143                 throw new FileNotFoundException();
    144             }
    145             for (final ZipEntry entry : parentList) {
    146                 addCursorRow(result, entry);
    147             }
    148         }
    149         return result;
    150     }
    151 
    152     /**
    153      * Returns a MIME type of a document within an archive.
    154      *
    155      * @see DocumentsProvider.getDocumentType(String)
    156      */
    157     public String getDocumentType(String documentId) throws FileNotFoundException {
    158         final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
    159         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
    160                 "Mismatching archive Uri. Expected: %s, actual: %s.");
    161 
    162         synchronized (mEntries) {
    163             final ZipEntry entry = mEntries.get(parsedId.mPath);
    164             if (entry == null) {
    165                 throw new FileNotFoundException();
    166             }
    167             return getMimeTypeForEntry(entry);
    168         }
    169     }
    170 
    171     /**
    172      * Returns true if a document within an archive is a child or any descendant of the archive
    173      * document or another document within the archive.
    174      *
    175      * @see DocumentsProvider.isChildDocument(String, String)
    176      */
    177     public boolean isChildDocument(String parentDocumentId, String documentId) {
    178         final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId);
    179         final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
    180         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
    181                 "Mismatching archive Uri. Expected: %s, actual: %s.");
    182 
    183         synchronized (mEntries) {
    184             final ZipEntry entry = mEntries.get(parsedId.mPath);
    185             if (entry == null) {
    186                 return false;
    187             }
    188 
    189             final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
    190             if (parentEntry == null || !parentEntry.isDirectory()) {
    191                 return false;
    192             }
    193 
    194             // Add a trailing slash even if it's not a directory, so it's easy to check if the
    195             // entry is a descendant.
    196             String pathWithSlash = entry.isDirectory() ? getEntryPath(entry)
    197                     : getEntryPath(entry) + "/";
    198 
    199             return pathWithSlash.startsWith(parsedParentId.mPath) &&
    200                     !parsedParentId.mPath.equals(pathWithSlash);
    201         }
    202     }
    203 
    204     /**
    205      * Returns metadata of a document within an archive.
    206      *
    207      * @see DocumentsProvider.queryDocument(String, String[])
    208      */
    209     public Cursor queryDocument(String documentId, @Nullable String[] projection)
    210             throws FileNotFoundException {
    211         final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
    212         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
    213                 "Mismatching archive Uri. Expected: %s, actual: %s.");
    214 
    215         synchronized (mEntries) {
    216             final ZipEntry entry = mEntries.get(parsedId.mPath);
    217             if (entry == null) {
    218                 throw new FileNotFoundException();
    219             }
    220 
    221             final MatrixCursor result = new MatrixCursor(
    222                     projection != null ? projection : DEFAULT_PROJECTION);
    223             if (mNotificationUri != null) {
    224                 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
    225             }
    226             addCursorRow(result, entry);
    227             return result;
    228         }
    229     }
    230 
    231     /**
    232      * Creates a file within an archive.
    233      *
    234      * @see DocumentsProvider.createDocument(String, String, String))
    235      */
    236     public String createDocument(String parentDocumentId, String mimeType, String displayName)
    237             throws FileNotFoundException {
    238         throw new UnsupportedOperationException("Creating documents not supported.");
    239     }
    240 
    241     /**
    242      * Opens a file within an archive.
    243      *
    244      * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
    245      */
    246     public ParcelFileDescriptor openDocument(
    247             String documentId, String mode, @Nullable final CancellationSignal signal)
    248             throws FileNotFoundException {
    249         throw new UnsupportedOperationException("Opening not supported.");
    250     }
    251 
    252     /**
    253      * Opens a thumbnail of a file within an archive.
    254      *
    255      * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
    256      */
    257     public AssetFileDescriptor openDocumentThumbnail(
    258             String documentId, Point sizeHint, final CancellationSignal signal)
    259             throws FileNotFoundException {
    260         throw new UnsupportedOperationException("Thumbnails not supported.");
    261     }
    262 
    263     /**
    264      * Creates an archive id for the passed path.
    265      */
    266     public ArchiveId createArchiveId(String path) {
    267         return new ArchiveId(mArchiveUri, mAccessMode, path);
    268     }
    269 
    270     /**
    271      * Not thread safe.
    272      */
    273     void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
    274         final MatrixCursor.RowBuilder row = cursor.newRow();
    275         final ArchiveId parsedId = createArchiveId(getEntryPath(entry));
    276         row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
    277 
    278         final File file = new File(entry.getName());
    279         row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
    280         row.add(Document.COLUMN_SIZE, entry.getSize());
    281 
    282         final String mimeType = getMimeTypeForEntry(entry);
    283         row.add(Document.COLUMN_MIME_TYPE, mimeType);
    284 
    285         int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
    286         if (MetadataReader.isSupportedMimeType(mimeType)) {
    287             flags |= Document.FLAG_SUPPORTS_METADATA;
    288         }
    289         row.add(Document.COLUMN_FLAGS, flags);
    290     }
    291 
    292     static String getMimeTypeForEntry(ZipEntry entry) {
    293         if (entry.isDirectory()) {
    294             return Document.MIME_TYPE_DIR;
    295         }
    296 
    297         final int lastDot = entry.getName().lastIndexOf('.');
    298         if (lastDot >= 0) {
    299             final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US);
    300             final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    301             if (mimeType != null) {
    302                 return mimeType;
    303             }
    304         }
    305 
    306         return "application/octet-stream";
    307     }
    308 
    309     // TODO: Upstream to the Preconditions class.
    310     // TODO: Move to a separate file.
    311     public static class MorePreconditions {
    312         static void checkArgumentEquals(String expected, @Nullable String actual,
    313                 String message) {
    314             if (!TextUtils.equals(expected, actual)) {
    315                 throw new IllegalArgumentException(String.format(message,
    316                         String.valueOf(expected), String.valueOf(actual)));
    317             }
    318         }
    319 
    320         static void checkArgumentEquals(Uri expected, @Nullable Uri actual,
    321                 String message) {
    322             checkArgumentEquals(expected.toString(), actual.toString(), message);
    323         }
    324     }
    325 };
    326