• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.documentsui.archives;
18 
19 import android.content.Context;
20 import android.content.res.AssetFileDescriptor;
21 import android.graphics.Point;
22 import android.media.ExifInterface;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.CancellationSignal;
26 import android.os.OperationCanceledException;
27 import android.os.ParcelFileDescriptor;
28 import android.os.storage.StorageManager;
29 import android.provider.DocumentsContract;
30 import android.support.annotation.Nullable;
31 import android.util.Log;
32 import android.util.jar.StrictJarFile;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.util.Preconditions;
36 
37 import libcore.io.IoUtils;
38 
39 import java.io.File;
40 import java.io.FileDescriptor;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.Iterator;
48 import java.util.List;
49 import java.util.Set;
50 import java.util.Stack;
51 import java.util.concurrent.TimeUnit;
52 import java.util.zip.ZipEntry;
53 
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  */
60 public class ReadableArchive extends Archive {
61     private static final String TAG = "ReadableArchive";
62 
63     private final StorageManager mStorageManager;
64     private final StrictJarFile mZipFile;
65 
ReadableArchive( Context context, @Nullable File file, @Nullable FileDescriptor fd, Uri archiveUri, int accessMode, @Nullable Uri notificationUri)66     private ReadableArchive(
67             Context context,
68             @Nullable File file,
69             @Nullable FileDescriptor fd,
70             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 
79         mStorageManager = mContext.getSystemService(StorageManager.class);
80 
81         mZipFile = file != null ?
82                 new StrictJarFile(file.getPath(), false /* verify */,
83                         false /* signatures */) :
84                 new StrictJarFile(fd, false /* verify */, false /* signatures */);
85 
86         ZipEntry entry;
87         String entryPath;
88         final Iterator<ZipEntry> it = mZipFile.iterator();
89         final Stack<ZipEntry> stack = new Stack<>();
90         while (it.hasNext()) {
91             entry = it.next();
92             if (entry.isDirectory() != entry.getName().endsWith("/")) {
93                 throw new IOException(
94                         "Directories must have a trailing slash, and files must not.");
95             }
96             entryPath = getEntryPath(entry);
97             if (mEntries.containsKey(entryPath)) {
98                 throw new IOException("Multiple entries with the same name are not supported.");
99             }
100             mEntries.put(entryPath, entry);
101             if (entry.isDirectory()) {
102                 mTree.put(entryPath, new ArrayList<ZipEntry>());
103             }
104             if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
105                 stack.push(entry);
106             }
107         }
108 
109         int delimiterIndex;
110         String parentPath;
111         ZipEntry parentEntry;
112         List<ZipEntry> parentList;
113 
114         // Go through all directories recursively and build a tree structure.
115         while (stack.size() > 0) {
116             entry = stack.pop();
117 
118             entryPath = getEntryPath(entry);
119             delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory()
120                     ? entryPath.length() - 2 : entryPath.length() - 1);
121             parentPath = entryPath.substring(0, delimiterIndex) + "/";
122 
123             parentList = mTree.get(parentPath);
124 
125             if (parentList == null) {
126                 // The ZIP file doesn't contain all directories leading to the entry.
127                 // It's rare, but can happen in a valid ZIP archive. In such case create a
128                 // fake ZipEntry and add it on top of the stack to process it next.
129                 parentEntry = new ZipEntry(parentPath);
130                 parentEntry.setSize(0);
131                 parentEntry.setTime(entry.getTime());
132                 mEntries.put(parentPath, parentEntry);
133 
134                 if (!"/".equals(parentPath)) {
135                     stack.push(parentEntry);
136                 }
137 
138                 parentList = new ArrayList<>();
139                 mTree.put(parentPath, parentList);
140             }
141 
142             parentList.add(entry);
143         }
144     }
145 
146     /**
147      * @see ParcelFileDescriptor
148      */
supportsAccessMode(int accessMode)149     public static boolean supportsAccessMode(int accessMode) {
150         return accessMode == ParcelFileDescriptor.MODE_READ_ONLY;
151     }
152 
153     /**
154      * Creates a DocumentsArchive instance for opening, browsing and accessing
155      * documents within the archive passed as a file descriptor.
156      *
157      * If the file descriptor is not seekable, then a snapshot will be created.
158      *
159      * This method takes ownership for the passed descriptor. The caller must
160      * not use it after passing.
161      *
162      * @param context Context of the provider.
163      * @param descriptor File descriptor for the archive's contents.
164      * @param archiveUri Uri of the archive document.
165      * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}.
166      * @param Uri notificationUri Uri for notifying that the archive file has changed.
167      */
createForParcelFileDescriptor( Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode, @Nullable Uri notificationUri)168     public static ReadableArchive createForParcelFileDescriptor(
169             Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode,
170             @Nullable Uri notificationUri)
171             throws IOException {
172         FileDescriptor fd = null;
173         try {
174             if (canSeek(descriptor)) {
175                 fd = new FileDescriptor();
176                 fd.setInt$(descriptor.detachFd());
177                 return new ReadableArchive(context, null, fd, archiveUri, accessMode,
178                         notificationUri);
179             }
180 
181             // 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                 }
205                 return new ReadableArchive(context, snapshotFile, null, archiveUri, accessMode,
206                         notificationUri);
207             } finally {
208                 // On UNIX the file will be still available for processes which opened it, even
209                 // after deleting it. Remove it ASAP, as it won't be used by anyone else.
210                 if (snapshotFile != null) {
211                     snapshotFile.delete();
212                 }
213             }
214         } catch (Exception e) {
215             // Since the method takes ownership of the passed descriptor, close it
216             // on exception.
217             IoUtils.closeQuietly(descriptor);
218             IoUtils.closeQuietly(fd);
219             throw e;
220         }
221     }
222 
223     @Override
openDocument( String documentId, String mode, @Nullable final CancellationSignal signal)224     public ParcelFileDescriptor openDocument(
225             String documentId, String mode, @Nullable final CancellationSignal signal)
226             throws FileNotFoundException {
227         MorePreconditions.checkArgumentEquals("r", mode,
228                 "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
229         final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
230         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
231                 "Mismatching archive Uri. Expected: %s, actual: %s.");
232 
233         final ZipEntry entry = mEntries.get(parsedId.mPath);
234         if (entry == null) {
235             throw new FileNotFoundException();
236         }
237 
238         try {
239             return mStorageManager.openProxyFileDescriptor(
240                     ParcelFileDescriptor.MODE_READ_ONLY, new Proxy(mZipFile, entry));
241         } catch (IOException e) {
242             throw new IllegalStateException(e);
243         }
244     }
245 
246     @Override
openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)247     public AssetFileDescriptor openDocumentThumbnail(
248             String documentId, Point sizeHint, final CancellationSignal signal)
249             throws FileNotFoundException {
250         final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
251         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
252                 "Mismatching archive Uri. Expected: %s, actual: %s.");
253         Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
254                 "Thumbnails only supported for image/* MIME type.");
255 
256         final ZipEntry entry = mEntries.get(parsedId.mPath);
257         if (entry == null) {
258             throw new FileNotFoundException();
259         }
260 
261         InputStream inputStream = null;
262         try {
263             inputStream = mZipFile.getInputStream(entry);
264             final ExifInterface exif = new ExifInterface(inputStream);
265             if (exif.hasThumbnail()) {
266                 Bundle extras = null;
267                 switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
268                     case ExifInterface.ORIENTATION_ROTATE_90:
269                         extras = new Bundle(1);
270                         extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90);
271                         break;
272                     case ExifInterface.ORIENTATION_ROTATE_180:
273                         extras = new Bundle(1);
274                         extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180);
275                         break;
276                     case ExifInterface.ORIENTATION_ROTATE_270:
277                         extras = new Bundle(1);
278                         extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270);
279                         break;
280                 }
281                 final long[] range = exif.getThumbnailRange();
282                 return new AssetFileDescriptor(
283                         openDocument(documentId, "r", signal), range[0], range[1], extras);
284             }
285         } catch (IOException e) {
286             // Ignore the exception, as reading the EXIF may legally fail.
287             Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e);
288         } finally {
289             IoUtils.closeQuietly(inputStream);
290         }
291 
292         return new AssetFileDescriptor(
293                 openDocument(documentId, "r", signal), 0, entry.getSize(), null);
294     }
295 
296     /**
297      * Closes an archive.
298      *
299      * <p>This method does not block until shutdown. Once called, other methods should not be
300      * called. Any active pipes will be terminated.
301      */
302     @Override
close()303     public void close() {
304         try {
305             mZipFile.close();
306         } catch (IOException e) {
307             // Silent close.
308         }
309     }
310 };
311