• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Archive( Context context, Uri archiveUri, int accessMode, @Nullable Uri notificationUri)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      */
getEntryPath(ZipEntry entry)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      */
canSeek(ParcelFileDescriptor descriptor)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      */
queryChildDocuments(String documentId, @Nullable String[] projection, @Nullable String sortOrder)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      */
getDocumentType(String documentId)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      */
isChildDocument(String parentDocumentId, String documentId)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      */
queryDocument(String documentId, @Nullable String[] projection)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      */
createDocument(String parentDocumentId, String mimeType, String displayName)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      */
openDocument( String documentId, String mode, @Nullable final CancellationSignal signal)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      */
openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)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      */
createArchiveId(String path)266     public ArchiveId createArchiveId(String path) {
267         return new ArchiveId(mArchiveUri, mAccessMode, path);
268     }
269 
270     /**
271      * Not thread safe.
272      */
addCursorRow(MatrixCursor cursor, ZipEntry entry)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 
getMimeTypeForEntry(ZipEntry entry)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 {
checkArgumentEquals(String expected, @Nullable String actual, String message)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 
checkArgumentEquals(Uri expected, @Nullable Uri actual, String message)320         static void checkArgumentEquals(Uri expected, @Nullable Uri actual,
321                 String message) {
322             checkArgumentEquals(expected.toString(), actual.toString(), message);
323         }
324     }
325 };
326