blob: e380729f97b687c192b8dbe4d7bb4f3b59392645 [file] [log] [blame]
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +09001/*
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
17package com.android.documentsui.archives;
18
19import android.content.Context;
20import android.content.res.AssetFileDescriptor;
21import android.database.Cursor;
22import android.database.MatrixCursor;
23import android.graphics.Point;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090024import android.net.Uri;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090025import android.os.CancellationSignal;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090026import android.os.ParcelFileDescriptor;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090027import android.provider.DocumentsContract.Document;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090028import android.support.annotation.Nullable;
Steve McKayccc18de2016-10-19 11:12:42 -070029import android.system.ErrnoException;
30import android.system.Os;
31import android.system.OsConstants;
32import android.text.TextUtils;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090033import android.webkit.MimeTypeMap;
34
Steve McKayccc18de2016-10-19 11:12:42 -070035import com.android.internal.util.Preconditions;
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +090036
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090037import java.io.Closeable;
38import java.io.File;
39import java.io.FileNotFoundException;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090040import java.util.HashMap;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090041import java.util.List;
42import java.util.Locale;
43import java.util.Map;
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +090044import java.util.concurrent.LinkedBlockingQueue;
45import java.util.concurrent.ThreadPoolExecutor;
46import java.util.concurrent.TimeUnit;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090047import java.util.zip.ZipEntry;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090048
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.
54 */
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090055public abstract class Archive implements Closeable {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090056 private static final String TAG = "Archive";
57
58 public static final String[] DEFAULT_PROJECTION = new String[] {
59 Document.COLUMN_DOCUMENT_ID,
60 Document.COLUMN_DISPLAY_NAME,
61 Document.COLUMN_MIME_TYPE,
62 Document.COLUMN_SIZE,
63 Document.COLUMN_FLAGS
64 };
65
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090066 final Context mContext;
67 final Uri mArchiveUri;
68 final int mArchiveMode;
69 final Uri mNotificationUri;
70 final ThreadPoolExecutor mExecutor;
71 final Map<String, ZipEntry> mEntries;
72 final Map<String, List<ZipEntry>> mTree;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090073
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090074 Archive(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090075 Context context,
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090076 Uri archiveUri,
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090077 int archiveMode,
78 @Nullable Uri notificationUri) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090079 mContext = context;
80 mArchiveUri = archiveUri;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090081 mArchiveMode = archiveMode;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090082 mNotificationUri = notificationUri;
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +090083
84 // At most 8 active threads. All threads idling for more than a minute will
85 // be closed.
86 mExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS,
Tomasz Mikolajewskie1fcc692016-12-02 10:29:38 +090087 new LinkedBlockingQueue<Runnable>());
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +090088 mExecutor.allowCoreThreadTimeOut(true);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090089
Steve McKayccc18de2016-10-19 11:12:42 -070090 mTree = new HashMap<>();
Steve McKayccc18de2016-10-19 11:12:42 -070091 mEntries = new HashMap<>();
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090092 }
93
94 /**
Tomasz Mikolajewski24c29fc2016-10-17 10:34:53 +090095 * Returns a valid, normalized path for an entry.
96 */
97 public static String getEntryPath(ZipEntry entry) {
98 Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
99 "Ill-formated ZIP-file.");
100 if (entry.getName().startsWith("/")) {
101 return entry.getName();
102 } else {
103 return "/" + entry.getName();
104 }
105 }
106
107 /**
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +0900108 * Returns true if the file descriptor is seekable.
109 * @param descriptor File descriptor to check.
110 */
111 public static boolean canSeek(ParcelFileDescriptor descriptor) {
112 try {
113 return Os.lseek(descriptor.getFileDescriptor(), 0,
Daichi Hironofefc6002016-12-15 12:36:00 +0900114 OsConstants.SEEK_CUR) == 0;
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +0900115 } catch (ErrnoException e) {
116 return false;
117 }
118 }
119
120 /**
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900121 * Lists child documents of an archive or a directory within an
122 * archive. Must be called only for archives with supported mime type,
123 * or for documents within archives.
124 *
125 * @see DocumentsProvider.queryChildDocuments(String, String[], String)
126 */
127 public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
128 @Nullable String sortOrder) throws FileNotFoundException {
129 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(documentId);
130 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
131 "Mismatching archive Uri. Expected: %s, actual: %s.");
132
133 final MatrixCursor result = new MatrixCursor(
134 projection != null ? projection : DEFAULT_PROJECTION);
135 if (mNotificationUri != null) {
136 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
137 }
138
139 final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath);
140 if (parentList == null) {
141 throw new FileNotFoundException();
142 }
143 for (final ZipEntry entry : parentList) {
144 addCursorRow(result, entry);
145 }
146 return result;
147 }
148
149 /**
150 * Returns a MIME type of a document within an archive.
151 *
152 * @see DocumentsProvider.getDocumentType(String)
153 */
154 public String getDocumentType(String documentId) throws FileNotFoundException {
155 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
156 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
157 "Mismatching archive Uri. Expected: %s, actual: %s.");
158
159 final ZipEntry entry = mEntries.get(parsedId.mPath);
160 if (entry == null) {
161 throw new FileNotFoundException();
162 }
163 return getMimeTypeForEntry(entry);
164 }
165
166 /**
167 * Returns true if a document within an archive is a child or any descendant of the archive
168 * document or another document within the archive.
169 *
170 * @see DocumentsProvider.isChildDocument(String, String)
171 */
172 public boolean isChildDocument(String parentDocumentId, String documentId) {
173 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId);
174 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
175 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
176 "Mismatching archive Uri. Expected: %s, actual: %s.");
177
178 final ZipEntry entry = mEntries.get(parsedId.mPath);
179 if (entry == null) {
180 return false;
181 }
182
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900183 final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
184 if (parentEntry == null || !parentEntry.isDirectory()) {
185 return false;
186 }
187
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900188 // Add a trailing slash even if it's not a directory, so it's easy to check if the
189 // entry is a descendant.
Tomasz Mikolajewski24c29fc2016-10-17 10:34:53 +0900190 String pathWithSlash = entry.isDirectory() ? getEntryPath(entry)
191 : getEntryPath(entry) + "/";
192
193 return pathWithSlash.startsWith(parsedParentId.mPath) &&
194 !parsedParentId.mPath.equals(pathWithSlash);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900195 }
196
197 /**
198 * Returns metadata of a document within an archive.
199 *
200 * @see DocumentsProvider.queryDocument(String, String[])
201 */
202 public Cursor queryDocument(String documentId, @Nullable String[] projection)
203 throws FileNotFoundException {
204 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
205 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
206 "Mismatching archive Uri. Expected: %s, actual: %s.");
207
208 final ZipEntry entry = mEntries.get(parsedId.mPath);
209 if (entry == null) {
210 throw new FileNotFoundException();
211 }
212
213 final MatrixCursor result = new MatrixCursor(
214 projection != null ? projection : DEFAULT_PROJECTION);
215 if (mNotificationUri != null) {
216 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
217 }
218 addCursorRow(result, entry);
219 return result;
220 }
221
222 /**
223 * Opens a file within an archive.
224 *
225 * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
226 */
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900227 abstract public ParcelFileDescriptor openDocument(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900228 String documentId, String mode, @Nullable final CancellationSignal signal)
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900229 throws FileNotFoundException;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900230
231 /**
232 * Opens a thumbnail of a file within an archive.
233 *
234 * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
235 */
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900236 abstract public AssetFileDescriptor openDocumentThumbnail(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900237 String documentId, Point sizeHint, final CancellationSignal signal)
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900238 throws FileNotFoundException;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900239
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900240 /**
241 * Creates an archive id for the passed path.
242 */
243 public ArchiveId createArchiveId(String path) {
244 return new ArchiveId(mArchiveUri, mArchiveMode, path);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900245 }
246
247 /**
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +0900248 * Closes an archive.
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900249 *
250 * <p>This method does not block until shutdown. Once called, other methods should not be
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +0900251 * called. Any active pipes will be terminated.
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900252 */
253 @Override
254 public void close() {
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +0900255 mExecutor.shutdownNow();
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900256 }
257
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900258 void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900259 final MatrixCursor.RowBuilder row = cursor.newRow();
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900260 final ArchiveId parsedId = createArchiveId(getEntryPath(entry));
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900261 row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
262
263 final File file = new File(entry.getName());
264 row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
265 row.add(Document.COLUMN_SIZE, entry.getSize());
266
267 final String mimeType = getMimeTypeForEntry(entry);
268 row.add(Document.COLUMN_MIME_TYPE, mimeType);
269
270 final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
271 row.add(Document.COLUMN_FLAGS, flags);
272 }
273
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900274 static String getMimeTypeForEntry(ZipEntry entry) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900275 if (entry.isDirectory()) {
276 return Document.MIME_TYPE_DIR;
277 }
278
279 final int lastDot = entry.getName().lastIndexOf('.');
280 if (lastDot >= 0) {
281 final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US);
282 final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
283 if (mimeType != null) {
284 return mimeType;
285 }
286 }
287
288 return "application/octet-stream";
289 }
290
291 // TODO: Upstream to the Preconditions class.
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900292 static class MorePreconditions {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900293 static void checkArgumentEquals(String expected, @Nullable String actual,
294 String message) {
295 if (!TextUtils.equals(expected, actual)) {
296 throw new IllegalArgumentException(String.format(message,
297 String.valueOf(expected), String.valueOf(actual)));
298 }
299 }
300
301 static void checkArgumentEquals(Uri expected, @Nullable Uri actual,
302 String message) {
303 checkArgumentEquals(expected.toString(), actual.toString(), message);
304 }
305 }
306};