blob: a8a9a9274d192596366896a2f27d849e1496104a [file] [log] [blame]
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +09001/*
2 * Copyright (C) 2017 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
felkachang1556c362018-08-30 16:21:59 +080019import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
20
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090021import android.content.Context;
22import android.content.res.AssetFileDescriptor;
23import android.graphics.Point;
24import android.media.ExifInterface;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.CancellationSignal;
felkachang1556c362018-08-30 16:21:59 +080028import android.os.FileUtils;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090029import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +090030import android.os.storage.StorageManager;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090031import android.provider.DocumentsContract;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090032import android.util.Log;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090033
felkachang1556c362018-08-30 16:21:59 +080034import androidx.annotation.Nullable;
Jeff Sharkey00a12bf2018-07-09 16:48:45 -060035import androidx.core.util.Preconditions;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090036
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090037import java.io.File;
felkachang1556c362018-08-30 16:21:59 +080038import java.io.FileInputStream;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090039import java.io.FileNotFoundException;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.io.InputStream;
felkachang1556c362018-08-30 16:21:59 +080043import java.nio.channels.FileChannel;
44import java.nio.channels.SeekableByteChannel;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090045import java.util.ArrayList;
felkachang1556c362018-08-30 16:21:59 +080046import java.util.Enumeration;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090047import java.util.List;
48import java.util.Stack;
felkachang1556c362018-08-30 16:21:59 +080049
50import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
51import org.apache.commons.compress.archivers.zip.ZipFile;
52import org.apache.commons.compress.utils.IOUtils;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090053
54/**
55 * Provides basic implementation for extracting and accessing
56 * files within archives exposed by a document provider.
57 *
58 * <p>This class is thread safe.
59 */
60public class ReadableArchive extends Archive {
Tomasz Mikolajewski7bb3bdc2017-01-26 09:56:59 +090061 private static final String TAG = "ReadableArchive";
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090062
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +090063 private final StorageManager mStorageManager;
felkachang1556c362018-08-30 16:21:59 +080064 private final ZipFile mZipFile;
65 private final ParcelFileDescriptor mParcelFileDescriptor;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090066
67 private ReadableArchive(
68 Context context,
felkachang1556c362018-08-30 16:21:59 +080069 @Nullable ParcelFileDescriptor parcelFileDescriptor,
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090070 Uri archiveUri,
71 int accessMode,
72 @Nullable Uri notificationUri)
73 throws IOException {
74 super(context, archiveUri, accessMode, notificationUri);
75 if (!supportsAccessMode(accessMode)) {
76 throw new IllegalStateException("Unsupported access mode.");
77 }
78
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +090079 mStorageManager = mContext.getSystemService(StorageManager.class);
Tomasz Mikolajewski3b135ef2017-01-25 15:58:10 +090080
felkachang1556c362018-08-30 16:21:59 +080081 if (parcelFileDescriptor == null || parcelFileDescriptor.getFileDescriptor() == null) {
82 throw new IllegalArgumentException("File descriptor is invalid");
83 }
84 mParcelFileDescriptor = parcelFileDescriptor;
85 mZipFile = openArchive(parcelFileDescriptor);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090086
felkachang1556c362018-08-30 16:21:59 +080087 ZipArchiveEntry entry;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090088 String entryPath;
felkachang1556c362018-08-30 16:21:59 +080089 final Enumeration<ZipArchiveEntry> it = mZipFile.getEntries();
90 final Stack<ZipArchiveEntry> stack = new Stack<>();
91 while (it.hasMoreElements()) {
92 entry = it.nextElement();
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +090093 if (entry.isDirectory() != entry.getName().endsWith("/")) {
94 throw new IOException(
95 "Directories must have a trailing slash, and files must not.");
96 }
97 entryPath = getEntryPath(entry);
98 if (mEntries.containsKey(entryPath)) {
99 throw new IOException("Multiple entries with the same name are not supported.");
100 }
101 mEntries.put(entryPath, entry);
102 if (entry.isDirectory()) {
felkachang1556c362018-08-30 16:21:59 +0800103 mTree.put(entryPath, new ArrayList<ZipArchiveEntry>());
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900104 }
105 if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
106 stack.push(entry);
107 }
108 }
109
110 int delimiterIndex;
111 String parentPath;
felkachang1556c362018-08-30 16:21:59 +0800112 ZipArchiveEntry parentEntry;
113 List<ZipArchiveEntry> parentList;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900114
115 // Go through all directories recursively and build a tree structure.
116 while (stack.size() > 0) {
117 entry = stack.pop();
118
119 entryPath = getEntryPath(entry);
120 delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory()
121 ? entryPath.length() - 2 : entryPath.length() - 1);
122 parentPath = entryPath.substring(0, delimiterIndex) + "/";
123
124 parentList = mTree.get(parentPath);
125
126 if (parentList == null) {
127 // The ZIP file doesn't contain all directories leading to the entry.
128 // It's rare, but can happen in a valid ZIP archive. In such case create a
129 // fake ZipEntry and add it on top of the stack to process it next.
felkachang1556c362018-08-30 16:21:59 +0800130 parentEntry = new ZipArchiveEntry(parentPath);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900131 parentEntry.setSize(0);
132 parentEntry.setTime(entry.getTime());
133 mEntries.put(parentPath, parentEntry);
134
135 if (!"/".equals(parentPath)) {
136 stack.push(parentEntry);
137 }
138
139 parentList = new ArrayList<>();
140 mTree.put(parentPath, parentList);
141 }
142
143 parentList.add(entry);
144 }
145 }
146
147 /**
felkachang1556c362018-08-30 16:21:59 +0800148 * To check the access mode is readable.
149 *
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900150 * @see ParcelFileDescriptor
151 */
152 public static boolean supportsAccessMode(int accessMode) {
felkachang1556c362018-08-30 16:21:59 +0800153 return accessMode == MODE_READ_ONLY;
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900154 }
155
156 /**
157 * Creates a DocumentsArchive instance for opening, browsing and accessing
158 * documents within the archive passed as a file descriptor.
felkachang1556c362018-08-30 16:21:59 +0800159 * <p>
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900160 * If the file descriptor is not seekable, then a snapshot will be created.
felkachang1556c362018-08-30 16:21:59 +0800161 * </p><p>
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900162 * This method takes ownership for the passed descriptor. The caller must
Tomasz Mikolajewskia903c2c2017-01-25 15:07:31 +0900163 * not use it after passing.
felkachang1556c362018-08-30 16:21:59 +0800164 * </p>
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900165 * @param context Context of the provider.
166 * @param descriptor File descriptor for the archive's contents.
167 * @param archiveUri Uri of the archive document.
168 * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}.
felkachang1556c362018-08-30 16:21:59 +0800169 * @param notificationUri notificationUri Uri for notifying that the archive file has changed.
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900170 */
171 public static ReadableArchive createForParcelFileDescriptor(
172 Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode,
173 @Nullable Uri notificationUri)
174 throws IOException {
felkachang1556c362018-08-30 16:21:59 +0800175 if (canSeek(descriptor)) {
176 return new ReadableArchive(context, descriptor, archiveUri, accessMode,
177 notificationUri);
178 }
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900179
felkachang1556c362018-08-30 16:21:59 +0800180 try {
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900181 // Fallback for non-seekable file descriptors.
182 File snapshotFile = null;
183 try {
184 // Create a copy of the archive, as ZipFile doesn't operate on streams.
185 // Moreover, ZipInputStream would be inefficient for large files on
186 // pipes.
187 snapshotFile = File.createTempFile("com.android.documentsui.snapshot{",
188 "}.zip", context.getCacheDir());
189
190 try (
191 final FileOutputStream outputStream =
192 new ParcelFileDescriptor.AutoCloseOutputStream(
193 ParcelFileDescriptor.open(
194 snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
195 final ParcelFileDescriptor.AutoCloseInputStream inputStream =
196 new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
197 ) {
198 final byte[] buffer = new byte[32 * 1024];
199 int bytes;
200 while ((bytes = inputStream.read(buffer)) != -1) {
201 outputStream.write(buffer, 0, bytes);
202 }
203 outputStream.flush();
204 }
felkachang1556c362018-08-30 16:21:59 +0800205
206 ParcelFileDescriptor snapshotPfd = ParcelFileDescriptor.open(
207 snapshotFile, MODE_READ_ONLY);
208
209 return new ReadableArchive(context, snapshotPfd, archiveUri, accessMode,
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900210 notificationUri);
211 } finally {
212 // On UNIX the file will be still available for processes which opened it, even
213 // after deleting it. Remove it ASAP, as it won't be used by anyone else.
214 if (snapshotFile != null) {
215 snapshotFile.delete();
216 }
217 }
218 } catch (Exception e) {
219 // Since the method takes ownership of the passed descriptor, close it
220 // on exception.
Jeff Sharkey94785ef2018-07-09 16:37:41 -0600221 FileUtils.closeQuietly(descriptor);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900222 throw e;
223 }
224 }
225
Tomasz Mikolajewski7bb3bdc2017-01-26 09:56:59 +0900226 @Override
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900227 public ParcelFileDescriptor openDocument(
228 String documentId, String mode, @Nullable final CancellationSignal signal)
229 throws FileNotFoundException {
230 MorePreconditions.checkArgumentEquals("r", mode,
231 "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
232 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
233 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
234 "Mismatching archive Uri. Expected: %s, actual: %s.");
235
felkachang1556c362018-08-30 16:21:59 +0800236 final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900237 if (entry == null) {
238 throw new FileNotFoundException();
239 }
240
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900241 try {
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +0900242 return mStorageManager.openProxyFileDescriptor(
felkachang1556c362018-08-30 16:21:59 +0800243 MODE_READ_ONLY, new Proxy(mZipFile, entry));
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900244 } catch (IOException e) {
Tomasz Mikolajewski5ed69832016-11-30 17:43:57 +0900245 throw new IllegalStateException(e);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900246 }
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900247 }
248
Tomasz Mikolajewski7bb3bdc2017-01-26 09:56:59 +0900249 @Override
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900250 public AssetFileDescriptor openDocumentThumbnail(
251 String documentId, Point sizeHint, final CancellationSignal signal)
252 throws FileNotFoundException {
253 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
254 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
255 "Mismatching archive Uri. Expected: %s, actual: %s.");
256 Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
257 "Thumbnails only supported for image/* MIME type.");
258
felkachang1556c362018-08-30 16:21:59 +0800259 final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900260 if (entry == null) {
261 throw new FileNotFoundException();
262 }
263
264 InputStream inputStream = null;
265 try {
266 inputStream = mZipFile.getInputStream(entry);
267 final ExifInterface exif = new ExifInterface(inputStream);
268 if (exif.hasThumbnail()) {
269 Bundle extras = null;
270 switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
271 case ExifInterface.ORIENTATION_ROTATE_90:
272 extras = new Bundle(1);
273 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90);
274 break;
275 case ExifInterface.ORIENTATION_ROTATE_180:
276 extras = new Bundle(1);
277 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180);
278 break;
279 case ExifInterface.ORIENTATION_ROTATE_270:
280 extras = new Bundle(1);
281 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270);
282 break;
283 }
284 final long[] range = exif.getThumbnailRange();
285 return new AssetFileDescriptor(
286 openDocument(documentId, "r", signal), range[0], range[1], extras);
287 }
288 } catch (IOException e) {
289 // Ignore the exception, as reading the EXIF may legally fail.
290 Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e);
291 } finally {
Jeff Sharkey94785ef2018-07-09 16:37:41 -0600292 FileUtils.closeQuietly(inputStream);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900293 }
294
295 return new AssetFileDescriptor(
296 openDocument(documentId, "r", signal), 0, entry.getSize(), null);
297 }
298
Tomasz Mikolajewski3b135ef2017-01-25 15:58:10 +0900299 /**
300 * Closes an archive.
301 *
302 * <p>This method does not block until shutdown. Once called, other methods should not be
303 * called. Any active pipes will be terminated.
304 */
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900305 @Override
306 public void close() {
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900307 try {
308 mZipFile.close();
309 } catch (IOException e) {
310 // Silent close.
felkachang1556c362018-08-30 16:21:59 +0800311 } finally {
312 /**
313 * For creating FileInputStream by using FileDescriptor, the file descriptor will not
314 * be closed after FileInputStream closed.
315 */
316 IOUtils.closeQuietly(mParcelFileDescriptor);
Tomasz Mikolajewskid683f972017-01-24 18:54:42 +0900317 }
318 }
felkachang1556c362018-08-30 16:21:59 +0800319
320 private static ZipFile openArchive(ParcelFileDescriptor parcelFileDescriptor)
321 throws IOException {
322 // TODO: To support multiple archive type
323
324 /**
325 * ZipFile keep the FileChannel instance as member field archive. FileChannel doesn't be
326 * closed until ZipFile.close(). FileChannel.close() invoke
327 * AbstractInterruptibleChannel.close() and then FileChannelImpl.implCloseChannel() is
328 * called. FileChannelImpl.implCloseChannel() will close the member field parent that is
329 * FileInputStream and is assigned in FileInputStream.getChannel().
330 * So, to close ZipFile is to close FileInputStream but not file descriptor.
331 */
332 FileChannel fileChannel = new FileInputStream(parcelFileDescriptor.getFileDescriptor())
333 .getChannel();
334 try {
335 return new ZipFile((SeekableByteChannel)fileChannel);
336 } catch (IOException e) {
337 IOUtils.closeQuietly(fileChannel);
338 IOUtils.closeQuietly(parcelFileDescriptor);
339 throw e;
340 }
341 }
342}