ReadableArchive.java revision 7bb3bdce2d2eb8e279073420151e48a4a0fb60c7
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 17package com.android.documentsui.archives; 18 19import android.content.Context; 20import android.content.res.AssetFileDescriptor; 21import android.graphics.Point; 22import android.media.ExifInterface; 23import android.net.Uri; 24import android.os.Bundle; 25import android.os.CancellationSignal; 26import android.os.OperationCanceledException; 27import android.os.ParcelFileDescriptor; 28import android.provider.DocumentsContract; 29import android.support.annotation.Nullable; 30import android.util.Log; 31import android.util.jar.StrictJarFile; 32 33import com.android.internal.annotations.GuardedBy; 34import com.android.internal.util.Preconditions; 35 36import libcore.io.IoUtils; 37 38import java.io.File; 39import java.io.FileDescriptor; 40import java.io.FileNotFoundException; 41import java.io.FileOutputStream; 42import java.io.IOException; 43import java.io.InputStream; 44import java.util.ArrayList; 45import java.util.HashSet; 46import java.util.Iterator; 47import java.util.List; 48import java.util.Set; 49import java.util.Stack; 50import java.util.concurrent.LinkedBlockingQueue; 51import java.util.concurrent.RejectedExecutionException; 52import java.util.concurrent.ThreadPoolExecutor; 53import java.util.concurrent.TimeUnit; 54import java.util.zip.ZipEntry; 55 56/** 57 * Provides basic implementation for extracting and accessing 58 * files within archives exposed by a document provider. 59 * 60 * <p>This class is thread safe. 61 */ 62public class ReadableArchive extends Archive { 63 private static final String TAG = "ReadableArchive"; 64 65 @GuardedBy("mEnqueuedOutputPipes") 66 private final Set<ParcelFileDescriptor> mEnqueuedOutputPipes = new HashSet<>(); 67 private final ThreadPoolExecutor mExecutor; 68 private final StrictJarFile mZipFile; 69 70 private ReadableArchive( 71 Context context, 72 @Nullable File file, 73 @Nullable FileDescriptor fd, 74 Uri archiveUri, 75 int accessMode, 76 @Nullable Uri notificationUri) 77 throws IOException { 78 super(context, archiveUri, accessMode, notificationUri); 79 if (!supportsAccessMode(accessMode)) { 80 throw new IllegalStateException("Unsupported access mode."); 81 } 82 83 // At most 8 active threads. All threads idling for more than a minute will 84 // be closed. 85 mExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS, 86 new LinkedBlockingQueue<Runnable>()); 87 mExecutor.allowCoreThreadTimeOut(true); 88 89 mZipFile = file != null ? 90 new StrictJarFile(file.getPath(), false /* verify */, 91 false /* signatures */) : 92 new StrictJarFile(fd, false /* verify */, false /* signatures */); 93 94 ZipEntry entry; 95 String entryPath; 96 final Iterator<ZipEntry> it = mZipFile.iterator(); 97 final Stack<ZipEntry> stack = new Stack<>(); 98 while (it.hasNext()) { 99 entry = it.next(); 100 if (entry.isDirectory() != entry.getName().endsWith("/")) { 101 throw new IOException( 102 "Directories must have a trailing slash, and files must not."); 103 } 104 entryPath = getEntryPath(entry); 105 if (mEntries.containsKey(entryPath)) { 106 throw new IOException("Multiple entries with the same name are not supported."); 107 } 108 mEntries.put(entryPath, entry); 109 if (entry.isDirectory()) { 110 mTree.put(entryPath, new ArrayList<ZipEntry>()); 111 } 112 if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent. 113 stack.push(entry); 114 } 115 } 116 117 int delimiterIndex; 118 String parentPath; 119 ZipEntry parentEntry; 120 List<ZipEntry> parentList; 121 122 // Go through all directories recursively and build a tree structure. 123 while (stack.size() > 0) { 124 entry = stack.pop(); 125 126 entryPath = getEntryPath(entry); 127 delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory() 128 ? entryPath.length() - 2 : entryPath.length() - 1); 129 parentPath = entryPath.substring(0, delimiterIndex) + "/"; 130 131 parentList = mTree.get(parentPath); 132 133 if (parentList == null) { 134 // The ZIP file doesn't contain all directories leading to the entry. 135 // It's rare, but can happen in a valid ZIP archive. In such case create a 136 // fake ZipEntry and add it on top of the stack to process it next. 137 parentEntry = new ZipEntry(parentPath); 138 parentEntry.setSize(0); 139 parentEntry.setTime(entry.getTime()); 140 mEntries.put(parentPath, parentEntry); 141 142 if (!"/".equals(parentPath)) { 143 stack.push(parentEntry); 144 } 145 146 parentList = new ArrayList<>(); 147 mTree.put(parentPath, parentList); 148 } 149 150 parentList.add(entry); 151 } 152 } 153 154 /** 155 * @see ParcelFileDescriptor 156 */ 157 public static boolean supportsAccessMode(int accessMode) { 158 return accessMode == ParcelFileDescriptor.MODE_READ_ONLY; 159 } 160 161 /** 162 * Creates a DocumentsArchive instance for opening, browsing and accessing 163 * documents within the archive passed as a file descriptor. 164 * 165 * If the file descriptor is not seekable, then a snapshot will be created. 166 * 167 * This method takes ownership for the passed descriptor. The caller must 168 * not close it. 169 * 170 * @param context Context of the provider. 171 * @param descriptor File descriptor for the archive's contents. 172 * @param archiveUri Uri of the archive document. 173 * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}. 174 * @param Uri notificationUri Uri for notifying that the archive file has changed. 175 */ 176 public static ReadableArchive createForParcelFileDescriptor( 177 Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode, 178 @Nullable Uri notificationUri) 179 throws IOException { 180 FileDescriptor fd = null; 181 try { 182 if (canSeek(descriptor)) { 183 fd = new FileDescriptor(); 184 fd.setInt$(descriptor.detachFd()); 185 return new ReadableArchive(context, null, fd, archiveUri, accessMode, 186 notificationUri); 187 } 188 189 // Fallback for non-seekable file descriptors. 190 File snapshotFile = null; 191 try { 192 // Create a copy of the archive, as ZipFile doesn't operate on streams. 193 // Moreover, ZipInputStream would be inefficient for large files on 194 // pipes. 195 snapshotFile = File.createTempFile("com.android.documentsui.snapshot{", 196 "}.zip", context.getCacheDir()); 197 198 try ( 199 final FileOutputStream outputStream = 200 new ParcelFileDescriptor.AutoCloseOutputStream( 201 ParcelFileDescriptor.open( 202 snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY)); 203 final ParcelFileDescriptor.AutoCloseInputStream inputStream = 204 new ParcelFileDescriptor.AutoCloseInputStream(descriptor); 205 ) { 206 final byte[] buffer = new byte[32 * 1024]; 207 int bytes; 208 while ((bytes = inputStream.read(buffer)) != -1) { 209 outputStream.write(buffer, 0, bytes); 210 } 211 outputStream.flush(); 212 } 213 return new ReadableArchive(context, snapshotFile, null, archiveUri, accessMode, 214 notificationUri); 215 } finally { 216 // On UNIX the file will be still available for processes which opened it, even 217 // after deleting it. Remove it ASAP, as it won't be used by anyone else. 218 if (snapshotFile != null) { 219 snapshotFile.delete(); 220 } 221 } 222 } catch (Exception e) { 223 // Since the method takes ownership of the passed descriptor, close it 224 // on exception. 225 IoUtils.closeQuietly(descriptor); 226 IoUtils.closeQuietly(fd); 227 throw e; 228 } 229 } 230 231 @Override 232 public ParcelFileDescriptor openDocument( 233 String documentId, String mode, @Nullable final CancellationSignal signal) 234 throws FileNotFoundException { 235 MorePreconditions.checkArgumentEquals("r", mode, 236 "Invalid mode. Only reading \"r\" supported, but got: \"%s\"."); 237 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 238 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, 239 "Mismatching archive Uri. Expected: %s, actual: %s."); 240 241 final ZipEntry entry = mEntries.get(parsedId.mPath); 242 if (entry == null) { 243 throw new FileNotFoundException(); 244 } 245 246 ParcelFileDescriptor[] pipe; 247 try { 248 pipe = ParcelFileDescriptor.createReliablePipe(); 249 } catch (IOException e) { 250 // Ideally we'd simply throw IOException to the caller, but for consistency 251 // with DocumentsProvider::openDocument, converting it to IllegalStateException. 252 throw new IllegalStateException("Failed to open the document.", e); 253 } 254 final InputStream inputStream = mZipFile.getInputStream(entry); 255 final ParcelFileDescriptor outputPipe = pipe[1]; 256 257 synchronized (mEnqueuedOutputPipes) { 258 mEnqueuedOutputPipes.add(outputPipe); 259 } 260 261 try { 262 mExecutor.execute( 263 new Runnable() { 264 @Override 265 public void run() { 266 synchronized (mEnqueuedOutputPipes) { 267 mEnqueuedOutputPipes.remove(outputPipe); 268 } 269 try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = 270 new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { 271 try { 272 final byte buffer[] = new byte[32 * 1024]; 273 int bytes; 274 while ((bytes = inputStream.read(buffer)) != -1) { 275 if (Thread.interrupted()) { 276 throw new InterruptedException(); 277 } 278 if (signal != null) { 279 signal.throwIfCanceled(); 280 } 281 outputStream.write(buffer, 0, bytes); 282 } 283 } catch (IOException | InterruptedException e) { 284 // Catch the exception before the outer try-with-resource closes 285 // the pipe with close() instead of closeWithError(). 286 try { 287 outputPipe.closeWithError(e.getMessage()); 288 } catch (IOException e2) { 289 Log.e(TAG, "Failed to close the pipe after an error.", e2); 290 } 291 } 292 } catch (OperationCanceledException e) { 293 // Cancelled gracefully. 294 } catch (IOException e) { 295 Log.e(TAG, "Failed to close the output stream gracefully.", e); 296 } finally { 297 IoUtils.closeQuietly(inputStream); 298 } 299 } 300 }); 301 } catch (RejectedExecutionException e) { 302 IoUtils.closeQuietly(pipe[0]); 303 IoUtils.closeQuietly(pipe[1]); 304 synchronized (mEnqueuedOutputPipes) { 305 mEnqueuedOutputPipes.remove(outputPipe); 306 } 307 throw new IllegalStateException("Failed to initialize pipe."); 308 } 309 310 return pipe[0]; 311 } 312 313 @Override 314 public AssetFileDescriptor openDocumentThumbnail( 315 String documentId, Point sizeHint, final CancellationSignal signal) 316 throws FileNotFoundException { 317 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 318 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, 319 "Mismatching archive Uri. Expected: %s, actual: %s."); 320 Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"), 321 "Thumbnails only supported for image/* MIME type."); 322 323 final ZipEntry entry = mEntries.get(parsedId.mPath); 324 if (entry == null) { 325 throw new FileNotFoundException(); 326 } 327 328 InputStream inputStream = null; 329 try { 330 inputStream = mZipFile.getInputStream(entry); 331 final ExifInterface exif = new ExifInterface(inputStream); 332 if (exif.hasThumbnail()) { 333 Bundle extras = null; 334 switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) { 335 case ExifInterface.ORIENTATION_ROTATE_90: 336 extras = new Bundle(1); 337 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90); 338 break; 339 case ExifInterface.ORIENTATION_ROTATE_180: 340 extras = new Bundle(1); 341 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180); 342 break; 343 case ExifInterface.ORIENTATION_ROTATE_270: 344 extras = new Bundle(1); 345 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270); 346 break; 347 } 348 final long[] range = exif.getThumbnailRange(); 349 return new AssetFileDescriptor( 350 openDocument(documentId, "r", signal), range[0], range[1], extras); 351 } 352 } catch (IOException e) { 353 // Ignore the exception, as reading the EXIF may legally fail. 354 Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e); 355 } finally { 356 IoUtils.closeQuietly(inputStream); 357 } 358 359 return new AssetFileDescriptor( 360 openDocument(documentId, "r", signal), 0, entry.getSize(), null); 361 } 362 363 /** 364 * Closes an archive. 365 * 366 * <p>This method does not block until shutdown. Once called, other methods should not be 367 * called. Any active pipes will be terminated. 368 */ 369 @Override 370 public void close() { 371 mExecutor.shutdownNow(); 372 synchronized (mEnqueuedOutputPipes) { 373 for (ParcelFileDescriptor outputPipe : mEnqueuedOutputPipes) { 374 try { 375 outputPipe.closeWithError("Archive closed."); 376 } catch (IOException e2) { 377 // Silent close. 378 } 379 } 380 } 381 try { 382 mZipFile.close(); 383 } catch (IOException e) { 384 // Silent close. 385 } 386 } 387}; 388