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.os.storage.StorageManager; 29import android.provider.DocumentsContract; 30import android.support.annotation.Nullable; 31import android.util.Log; 32import android.util.jar.StrictJarFile; 33 34import com.android.internal.annotations.GuardedBy; 35import com.android.internal.util.Preconditions; 36 37import libcore.io.IoUtils; 38 39import java.io.File; 40import java.io.FileDescriptor; 41import java.io.FileNotFoundException; 42import java.io.FileOutputStream; 43import java.io.IOException; 44import java.io.InputStream; 45import java.util.ArrayList; 46import java.util.HashSet; 47import java.util.Iterator; 48import java.util.List; 49import java.util.Set; 50import java.util.Stack; 51import java.util.concurrent.TimeUnit; 52import 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 */ 60public class ReadableArchive extends Archive { 61 private static final String TAG = "ReadableArchive"; 62 63 private final StorageManager mStorageManager; 64 private final StrictJarFile mZipFile; 65 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 */ 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 */ 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 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 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 303 public void close() { 304 try { 305 mZipFile.close(); 306 } catch (IOException e) { 307 // Silent close. 308 } 309 } 310}; 311