blob: 5193063880c358a94a2e1ab96bdb0c91b5cfb8f8 [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;
felkachang1556c362018-08-30 16:21:59 +080028import android.provider.MetadataReader;
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
Jeff Sharkeya4ff00f2018-07-09 14:57:51 -060035import androidx.annotation.GuardedBy;
felkachang1556c362018-08-30 16:21:59 +080036import androidx.annotation.Nullable;
Jeff Sharkey00a12bf2018-07-09 16:48:45 -060037import androidx.core.util.Preconditions;
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +090038
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090039import java.io.Closeable;
40import java.io.File;
41import java.io.FileNotFoundException;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090042import java.util.HashMap;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090043import java.util.List;
44import java.util.Locale;
45import java.util.Map;
felkachang1556c362018-08-30 16:21:59 +080046
47import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
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;
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +090068 final int mAccessMode;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090069 final Uri mNotificationUri;
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +090070
71 // The container as well as values are guarded by mEntries.
72 @GuardedBy("mEntries")
felkachang1556c362018-08-30 16:21:59 +080073 final Map<String, ZipArchiveEntry> mEntries;
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +090074
75 // The container as well as values and elements of values are guarded by mEntries.
76 @GuardedBy("mEntries")
felkachang1556c362018-08-30 16:21:59 +080077 final Map<String, List<ZipArchiveEntry>> mTree;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090078
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090079 Archive(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090080 Context context,
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090081 Uri archiveUri,
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +090082 int accessMode,
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090083 @Nullable Uri notificationUri) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090084 mContext = context;
85 mArchiveUri = archiveUri;
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +090086 mAccessMode = accessMode;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090087 mNotificationUri = notificationUri;
Tomasz Mikolajewskib33955b2016-11-30 16:08:04 +090088
Steve McKayccc18de2016-10-19 11:12:42 -070089 mTree = new HashMap<>();
Steve McKayccc18de2016-10-19 11:12:42 -070090 mEntries = new HashMap<>();
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090091 }
92
93 /**
Tomasz Mikolajewski24c29fc2016-10-17 10:34:53 +090094 * Returns a valid, normalized path for an entry.
95 */
felkachang1556c362018-08-30 16:21:59 +080096 public static String getEntryPath(ZipArchiveEntry entry) {
Tomasz Mikolajewski24c29fc2016-10-17 10:34:53 +090097 Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
98 "Ill-formated ZIP-file.");
99 if (entry.getName().startsWith("/")) {
100 return entry.getName();
101 } else {
102 return "/" + entry.getName();
103 }
104 }
105
106 /**
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +0900107 * Returns true if the file descriptor is seekable.
108 * @param descriptor File descriptor to check.
109 */
110 public static boolean canSeek(ParcelFileDescriptor descriptor) {
111 try {
112 return Os.lseek(descriptor.getFileDescriptor(), 0,
Daichi Hironofefc6002016-12-15 12:36:00 +0900113 OsConstants.SEEK_CUR) == 0;
Tomasz Mikolajewski977cf482016-10-12 14:51:39 +0900114 } catch (ErrnoException e) {
115 return false;
116 }
117 }
118
119 /**
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900120 * Lists child documents of an archive or a directory within an
121 * archive. Must be called only for archives with supported mime type,
122 * or for documents within archives.
123 *
124 * @see DocumentsProvider.queryChildDocuments(String, String[], String)
125 */
126 public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
127 @Nullable String sortOrder) throws FileNotFoundException {
128 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(documentId);
129 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
130 "Mismatching archive Uri. Expected: %s, actual: %s.");
131
132 final MatrixCursor result = new MatrixCursor(
133 projection != null ? projection : DEFAULT_PROJECTION);
134 if (mNotificationUri != null) {
135 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
136 }
137
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900138 synchronized (mEntries) {
felkachang1556c362018-08-30 16:21:59 +0800139 final List<ZipArchiveEntry> parentList = mTree.get(parsedParentId.mPath);
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900140 if (parentList == null) {
141 throw new FileNotFoundException();
142 }
felkachang1556c362018-08-30 16:21:59 +0800143 for (final ZipArchiveEntry entry : parentList) {
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900144 addCursorRow(result, entry);
145 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900146 }
147 return result;
148 }
149
150 /**
151 * Returns a MIME type of a document within an archive.
152 *
153 * @see DocumentsProvider.getDocumentType(String)
154 */
155 public String getDocumentType(String documentId) throws FileNotFoundException {
156 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
157 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
158 "Mismatching archive Uri. Expected: %s, actual: %s.");
159
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900160 synchronized (mEntries) {
felkachang1556c362018-08-30 16:21:59 +0800161 final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900162 if (entry == null) {
163 throw new FileNotFoundException();
164 }
165 return getMimeTypeForEntry(entry);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900166 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900167 }
168
169 /**
170 * Returns true if a document within an archive is a child or any descendant of the archive
171 * document or another document within the archive.
172 *
173 * @see DocumentsProvider.isChildDocument(String, String)
174 */
175 public boolean isChildDocument(String parentDocumentId, String documentId) {
176 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId);
177 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
178 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
179 "Mismatching archive Uri. Expected: %s, actual: %s.");
180
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900181 synchronized (mEntries) {
felkachang1556c362018-08-30 16:21:59 +0800182 final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900183 if (entry == null) {
184 return false;
185 }
186
felkachang1556c362018-08-30 16:21:59 +0800187 final ZipArchiveEntry parentEntry = mEntries.get(parsedParentId.mPath);
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900188 if (parentEntry == null || !parentEntry.isDirectory()) {
189 return false;
190 }
191
192 // Add a trailing slash even if it's not a directory, so it's easy to check if the
193 // entry is a descendant.
194 String pathWithSlash = entry.isDirectory() ? getEntryPath(entry)
195 : getEntryPath(entry) + "/";
196
197 return pathWithSlash.startsWith(parsedParentId.mPath) &&
198 !parsedParentId.mPath.equals(pathWithSlash);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900199 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900200 }
201
202 /**
203 * Returns metadata of a document within an archive.
204 *
205 * @see DocumentsProvider.queryDocument(String, String[])
206 */
207 public Cursor queryDocument(String documentId, @Nullable String[] projection)
208 throws FileNotFoundException {
209 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
210 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
211 "Mismatching archive Uri. Expected: %s, actual: %s.");
212
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900213 synchronized (mEntries) {
felkachang1556c362018-08-30 16:21:59 +0800214 final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900215 if (entry == null) {
216 throw new FileNotFoundException();
217 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900218
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900219 final MatrixCursor result = new MatrixCursor(
220 projection != null ? projection : DEFAULT_PROJECTION);
221 if (mNotificationUri != null) {
222 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
223 }
224 addCursorRow(result, entry);
225 return result;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900226 }
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900227 }
228
229 /**
230 * Creates a file within an archive.
231 *
232 * @see DocumentsProvider.createDocument(String, String, String))
233 */
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900234 public String createDocument(String parentDocumentId, String mimeType, String displayName)
235 throws FileNotFoundException {
236 throw new UnsupportedOperationException("Creating documents not supported.");
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900237 }
238
239 /**
240 * Opens a file within an archive.
241 *
242 * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
243 */
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900244 public ParcelFileDescriptor openDocument(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900245 String documentId, String mode, @Nullable final CancellationSignal signal)
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900246 throws FileNotFoundException {
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +0900247 throw new UnsupportedOperationException("Opening not supported.");
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900248 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900249
250 /**
251 * Opens a thumbnail of a file within an archive.
252 *
253 * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
254 */
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900255 public AssetFileDescriptor openDocumentThumbnail(
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900256 String documentId, Point sizeHint, final CancellationSignal signal)
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900257 throws FileNotFoundException {
258 throw new UnsupportedOperationException("Thumbnails not supported.");
259 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900260
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900261 /**
262 * Creates an archive id for the passed path.
263 */
264 public ArchiveId createArchiveId(String path) {
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900265 return new ArchiveId(mArchiveUri, mAccessMode, path);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900266 }
267
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900268 /**
269 * Not thread safe.
270 */
felkachang1556c362018-08-30 16:21:59 +0800271 void addCursorRow(MatrixCursor cursor, ZipArchiveEntry entry) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900272 final MatrixCursor.RowBuilder row = cursor.newRow();
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900273 final ArchiveId parsedId = createArchiveId(getEntryPath(entry));
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900274 row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
275
276 final File file = new File(entry.getName());
277 row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
278 row.add(Document.COLUMN_SIZE, entry.getSize());
279
280 final String mimeType = getMimeTypeForEntry(entry);
281 row.add(Document.COLUMN_MIME_TYPE, mimeType);
282
Steve McKay21b4a272017-07-20 11:40:24 -0700283 int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
284 if (MetadataReader.isSupportedMimeType(mimeType)) {
285 flags |= Document.FLAG_SUPPORTS_METADATA;
286 }
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900287 row.add(Document.COLUMN_FLAGS, flags);
288 }
289
felkachang1556c362018-08-30 16:21:59 +0800290 static String getMimeTypeForEntry(ZipArchiveEntry entry) {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900291 if (entry.isDirectory()) {
292 return Document.MIME_TYPE_DIR;
293 }
294
295 final int lastDot = entry.getName().lastIndexOf('.');
296 if (lastDot >= 0) {
297 final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US);
298 final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
299 if (mimeType != null) {
300 return mimeType;
301 }
302 }
303
304 return "application/octet-stream";
305 }
306
307 // TODO: Upstream to the Preconditions class.
Tomasz Mikolajewskie731da62017-01-30 11:07:04 +0900308 // TODO: Move to a separate file.
309 public static class MorePreconditions {
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900310 static void checkArgumentEquals(String expected, @Nullable String actual,
311 String message) {
312 if (!TextUtils.equals(expected, actual)) {
313 throw new IllegalArgumentException(String.format(message,
314 String.valueOf(expected), String.valueOf(actual)));
315 }
316 }
317
318 static void checkArgumentEquals(Uri expected, @Nullable Uri actual,
319 String message) {
320 checkArgumentEquals(expected.toString(), actual.toString(), message);
321 }
322 }
323};