ReadableArchive.java revision a903c2cdad65d0fe163a4152faa6bd05e2888b22
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 use it after passing. 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 Log.e(TAG, "Failed while reading a file.", e); 288 outputPipe.closeWithError("Reading failure."); 289 } catch (IOException e2) { 290 Log.e(TAG, "Failed to close the pipe after an error.", e2); 291 } 292 } 293 } catch (OperationCanceledException e) { 294 // Cancelled gracefully. 295 } catch (IOException e) { 296 Log.e(TAG, "Failed to close the output stream gracefully.", e); 297 } finally { 298 IoUtils.closeQuietly(inputStream); 299 } 300 } 301 }); 302 } catch (RejectedExecutionException e) { 303 IoUtils.closeQuietly(pipe[0]); 304 IoUtils.closeQuietly(pipe[1]); 305 synchronized (mEnqueuedOutputPipes) { 306 mEnqueuedOutputPipes.remove(outputPipe); 307 } 308 throw new IllegalStateException("Failed to initialize pipe."); 309 } 310 311 return pipe[0]; 312 } 313 314 @Override 315 public AssetFileDescriptor openDocumentThumbnail( 316 String documentId, Point sizeHint, final CancellationSignal signal) 317 throws FileNotFoundException { 318 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 319 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, 320 "Mismatching archive Uri. Expected: %s, actual: %s."); 321 Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"), 322 "Thumbnails only supported for image/* MIME type."); 323 324 final ZipEntry entry = mEntries.get(parsedId.mPath); 325 if (entry == null) { 326 throw new FileNotFoundException(); 327 } 328 329 InputStream inputStream = null; 330 try { 331 inputStream = mZipFile.getInputStream(entry); 332 final ExifInterface exif = new ExifInterface(inputStream); 333 if (exif.hasThumbnail()) { 334 Bundle extras = null; 335 switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) { 336 case ExifInterface.ORIENTATION_ROTATE_90: 337 extras = new Bundle(1); 338 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90); 339 break; 340 case ExifInterface.ORIENTATION_ROTATE_180: 341 extras = new Bundle(1); 342 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180); 343 break; 344 case ExifInterface.ORIENTATION_ROTATE_270: 345 extras = new Bundle(1); 346 extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270); 347 break; 348 } 349 final long[] range = exif.getThumbnailRange(); 350 return new AssetFileDescriptor( 351 openDocument(documentId, "r", signal), range[0], range[1], extras); 352 } 353 } catch (IOException e) { 354 // Ignore the exception, as reading the EXIF may legally fail. 355 Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e); 356 } finally { 357 IoUtils.closeQuietly(inputStream); 358 } 359 360 return new AssetFileDescriptor( 361 openDocument(documentId, "r", signal), 0, entry.getSize(), null); 362 } 363 364 /** 365 * Closes an archive. 366 * 367 * <p>This method does not block until shutdown. Once called, other methods should not be 368 * called. Any active pipes will be terminated. 369 */ 370 @Override 371 public void close() { 372 mExecutor.shutdownNow(); 373 synchronized (mEnqueuedOutputPipes) { 374 for (ParcelFileDescriptor outputPipe : mEnqueuedOutputPipes) { 375 try { 376 outputPipe.closeWithError("Archive closed."); 377 } catch (IOException e2) { 378 // Silent close. 379 } 380 } 381 } 382 try { 383 mZipFile.close(); 384 } catch (IOException e) { 385 // Silent close. 386 } 387 } 388}; 389