Loading src/com/android/documentsui/DocumentsAccess.java +3 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Path; Loading Loading @@ -107,7 +108,8 @@ public interface DocumentsAccess { @Override public DocumentInfo getArchiveDocument(Uri uri) { return getDocument(ArchivesProvider.buildUriForArchive(uri)); return getDocument(ArchivesProvider.buildUriForArchive(uri, ParcelFileDescriptor.MODE_READ_ONLY)); } @Override Loading src/com/android/documentsui/archives/Archive.java +25 −285 Original line number Diff line number Diff line Loading @@ -21,44 +21,26 @@ import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Point; import android.media.ExifInterface; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; import android.util.Log; import android.util.jar.StrictJarFile; import android.webkit.MimeTypeMap; import com.android.internal.util.Preconditions; import libcore.io.IoUtils; import java.io.Closeable; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Stack; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; Loading @@ -70,7 +52,7 @@ import java.util.zip.ZipEntry; * * <p>This class is thread safe. */ public class Archive implements Closeable { public abstract class Archive implements Closeable { private static final String TAG = "Archive"; public static final String[] DEFAULT_PROJECTION = new String[] { Loading @@ -81,27 +63,23 @@ public class Archive implements Closeable { Document.COLUMN_FLAGS }; private final Context mContext; private final Uri mArchiveUri; private final Uri mNotificationUri; private final StrictJarFile mZipFile; private final ThreadPoolExecutor mExecutor; private final Map<String, ZipEntry> mEntries; private final Map<String, List<ZipEntry>> mTree; final Context mContext; final Uri mArchiveUri; final int mArchiveMode; final Uri mNotificationUri; final ThreadPoolExecutor mExecutor; final Map<String, ZipEntry> mEntries; final Map<String, List<ZipEntry>> mTree; private Archive( Archive( Context context, @Nullable File file, @Nullable FileDescriptor fd, Uri archiveUri, @Nullable Uri notificationUri) throws IOException { int archiveMode, @Nullable Uri notificationUri) { mContext = context; mArchiveUri = archiveUri; mArchiveMode = archiveMode; mNotificationUri = notificationUri; mZipFile = file != null ? new StrictJarFile(file.getPath(), false /* verify */, false /* signatures */) : new StrictJarFile(fd, false /* verify */, false /* signatures */); // At most 8 active threads. All threads idling for more than a minute will // be closed. Loading @@ -109,68 +87,8 @@ public class Archive implements Closeable { new LinkedBlockingQueue<Runnable>()); mExecutor.allowCoreThreadTimeOut(true); // Build the tree structure in memory. mTree = new HashMap<>(); mEntries = new HashMap<>(); ZipEntry entry; String entryPath; final Iterator<ZipEntry> it = mZipFile.iterator(); final Stack<ZipEntry> stack = new Stack<>(); while (it.hasNext()) { entry = it.next(); if (entry.isDirectory() != entry.getName().endsWith("/")) { throw new IOException( "Directories must have a trailing slash, and files must not."); } entryPath = getEntryPath(entry); if (mEntries.containsKey(entryPath)) { throw new IOException("Multiple entries with the same name are not supported."); } mEntries.put(entryPath, entry); if (entry.isDirectory()) { mTree.put(entryPath, new ArrayList<ZipEntry>()); } if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent. stack.push(entry); } } int delimiterIndex; String parentPath; ZipEntry parentEntry; List<ZipEntry> parentList; // Go through all directories recursively and build a tree structure. while (stack.size() > 0) { entry = stack.pop(); entryPath = getEntryPath(entry); delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory() ? entryPath.length() - 2 : entryPath.length() - 1); parentPath = entryPath.substring(0, delimiterIndex) + "/"; parentList = mTree.get(parentPath); if (parentList == null) { // The ZIP file doesn't contain all directories leading to the entry. // It's rare, but can happen in a valid ZIP archive. In such case create a // fake ZipEntry and add it on top of the stack to process it next. parentEntry = new ZipEntry(parentPath); parentEntry.setSize(0); parentEntry.setTime(entry.getTime()); mEntries.put(parentPath, parentEntry); if (!"/".equals(parentPath)) { stack.push(parentEntry); } parentList = new ArrayList<>(); mTree.put(parentPath, parentList); } parentList.add(entry); } } /** Loading @@ -190,7 +108,6 @@ public class Archive implements Closeable { * Returns true if the file descriptor is seekable. * @param descriptor File descriptor to check. */ @VisibleForTesting public static boolean canSeek(ParcelFileDescriptor descriptor) { try { return Os.lseek(descriptor.getFileDescriptor(), 0, Loading @@ -200,75 +117,6 @@ public class Archive implements Closeable { } } /** * Creates a DocumentsArchive instance for opening, browsing and accessing * documents within the archive passed as a file descriptor. * * If the file descriptor is not seekable, then a snapshot will be created. * * This method takes ownership for the passed descriptor. The caller must * not close it. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. * @param archiveUri Uri of the archive document. * @param Uri notificationUri Uri for notifying that the archive file has changed. */ public static Archive createForParcelFileDescriptor( Context context, ParcelFileDescriptor descriptor, Uri archiveUri, @Nullable Uri notificationUri) throws IOException { FileDescriptor fd = null; try { if (canSeek(descriptor)) { fd = new FileDescriptor(); fd.setInt$(descriptor.detachFd()); return new Archive(context, null, fd, archiveUri, notificationUri); } // Fallback for non-seekable file descriptors. File snapshotFile = null; try { // Create a copy of the archive, as ZipFile doesn't operate on streams. // Moreover, ZipInputStream would be inefficient for large files on // pipes. snapshotFile = File.createTempFile("com.android.documentsui.snapshot{", "}.zip", context.getCacheDir()); try ( final FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( ParcelFileDescriptor.open( snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY)); final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor); ) { final byte[] buffer = new byte[32 * 1024]; int bytes; while ((bytes = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytes); } outputStream.flush(); } return new Archive(context, snapshotFile, null, archiveUri, notificationUri); } finally { // On UNIX the file will be still available for processes which opened it, even // after deleting it. Remove it ASAP, as it won't be used by anyone else. if (snapshotFile != null) { snapshotFile.delete(); } } } catch (Exception e) { // Since the method takes ownership of the passed descriptor, close it // on exception. IoUtils.closeQuietly(descriptor); IoUtils.closeQuietly(fd); throw e; } } /** * Lists child documents of an archive or a directory within an * archive. Must be called only for archives with supported mime type, Loading Loading @@ -376,127 +224,24 @@ public class Archive implements Closeable { * * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) */ public ParcelFileDescriptor openDocument( abstract public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { MorePreconditions.checkArgumentEquals("r", mode, "Invalid mode. Only reading \"r\" supported, but got: \"%s\"."); final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } ParcelFileDescriptor[] pipe; InputStream inputStream = null; try { pipe = ParcelFileDescriptor.createReliablePipe(); inputStream = mZipFile.getInputStream(entry); } catch (IOException e) { if (inputStream != null) { IoUtils.closeQuietly(inputStream); } // Ideally we'd simply throw IOException to the caller, but for consistency // with DocumentsProvider::openDocument, converting it to IllegalStateException. throw new IllegalStateException("Failed to open the document.", e); } final ParcelFileDescriptor outputPipe = pipe[1]; final InputStream finalInputStream = inputStream; mExecutor.execute( new Runnable() { @Override public void run() { try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { try { final byte buffer[] = new byte[32 * 1024]; int bytes; while ((bytes = finalInputStream.read(buffer)) != -1) { if (Thread.interrupted()) { throw new InterruptedException(); } if (signal != null) { signal.throwIfCanceled(); } outputStream.write(buffer, 0, bytes); } } catch (IOException | InterruptedException e) { // Catch the exception before the outer try-with-resource closes the // pipe with close() instead of closeWithError(). try { outputPipe.closeWithError(e.getMessage()); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { Log.e(TAG, "Failed to close the output stream gracefully.", e); } finally { IoUtils.closeQuietly(finalInputStream); } } }); return pipe[0]; } throws FileNotFoundException; /** * Opens a thumbnail of a file within an archive. * * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) */ public AssetFileDescriptor openDocumentThumbnail( abstract public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException { final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"), "Thumbnails only supported for image/* MIME type."); final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } throws FileNotFoundException; InputStream inputStream = null; try { inputStream = mZipFile.getInputStream(entry); final ExifInterface exif = new ExifInterface(inputStream); if (exif.hasThumbnail()) { Bundle extras = null; switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) { case ExifInterface.ORIENTATION_ROTATE_90: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90); break; case ExifInterface.ORIENTATION_ROTATE_180: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180); break; case ExifInterface.ORIENTATION_ROTATE_270: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270); break; } final long[] range = exif.getThumbnailRange(); return new AssetFileDescriptor( openDocument(documentId, "r", signal), range[0], range[1], extras); } } catch (IOException e) { // Ignore the exception, as reading the EXIF may legally fail. Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e); } finally { IoUtils.closeQuietly(inputStream); } return new AssetFileDescriptor( openDocument(documentId, "r", signal), 0, entry.getSize(), null); /** * Creates an archive id for the passed path. */ public ArchiveId createArchiveId(String path) { return new ArchiveId(mArchiveUri, mArchiveMode, path); } /** Loading @@ -508,16 +253,11 @@ public class Archive implements Closeable { @Override public void close() { mExecutor.shutdownNow(); try { mZipFile.close(); } catch (IOException e) { // Silent close. } } private void addCursorRow(MatrixCursor cursor, ZipEntry entry) { void addCursorRow(MatrixCursor cursor, ZipEntry entry) { final MatrixCursor.RowBuilder row = cursor.newRow(); final ArchiveId parsedId = new ArchiveId(mArchiveUri, getEntryPath(entry)); final ArchiveId parsedId = createArchiveId(getEntryPath(entry)); row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId()); final File file = new File(entry.getName()); Loading @@ -531,7 +271,7 @@ public class Archive implements Closeable { row.add(Document.COLUMN_FLAGS, flags); } private String getMimeTypeForEntry(ZipEntry entry) { static String getMimeTypeForEntry(ZipEntry entry) { if (entry.isDirectory()) { return Document.MIME_TYPE_DIR; } Loading @@ -549,7 +289,7 @@ public class Archive implements Closeable { } // TODO: Upstream to the Preconditions class. private static class MorePreconditions { static class MorePreconditions { static void checkArgumentEquals(String expected, @Nullable String actual, String message) { if (!TextUtils.equals(expected, actual)) { Loading src/com/android/documentsui/archives/ArchiveId.java +19 −5 Original line number Diff line number Diff line Loading @@ -22,22 +22,36 @@ public class ArchiveId { private final static char DELIMITER = '#'; public final Uri mArchiveUri; public final int mAccessMode; public final String mPath; public ArchiveId(Uri archiveUri, String path) { public ArchiveId(Uri archiveUri, int accessMode, String path) { assert(archiveUri.toString().indexOf(DELIMITER) == -1); assert(!path.isEmpty()); mArchiveUri = archiveUri; mAccessMode = accessMode; mPath = path; assert(!mPath.isEmpty()); } static public ArchiveId fromDocumentId(String documentId) { final int delimiterPosition = documentId.indexOf(DELIMITER); assert(delimiterPosition != -1); return new ArchiveId(Uri.parse(documentId.substring(0, delimiterPosition)), documentId.substring((delimiterPosition + 1))); final int secondDelimiterPosition = documentId.indexOf(DELIMITER, delimiterPosition + 1); assert(secondDelimiterPosition != -1); final String archiveUriPart = documentId.substring(0, delimiterPosition); final String accessModePart = documentId.substring(delimiterPosition + 1, secondDelimiterPosition); final String pathPart = documentId.substring(secondDelimiterPosition + 1); return new ArchiveId(Uri.parse(archiveUriPart), Integer.parseInt(accessModePart), pathPart); } public String toDocumentId() { return mArchiveUri.toString() + DELIMITER + mPath; return mArchiveUri.toString() + DELIMITER + mAccessMode + DELIMITER + mPath; } }; src/com/android/documentsui/archives/ArchivesProvider.java +13 −7 Original line number Diff line number Diff line Loading @@ -121,7 +121,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { cursor.setExtras(bundle); cursor.setNotificationUri(getContext().getContentResolver(), buildUriForArchive(archiveId.mArchiveUri)); buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); return cursor; } finally { releaseInstance(loader); Loading Loading @@ -231,9 +231,15 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { return false; } public static Uri buildUriForArchive(Uri archiveUri) { return DocumentsContract.buildDocumentUri( AUTHORITY, new ArchiveId(archiveUri, "/").toDocumentId()); /** * Creates a Uri for accessing an archive with the specified access mode. * * @see ParcelFileDescriptor#MODE_READ * @see ParcelFileDescriptor#MODE_WRITE */ public static Uri buildUriForArchive(Uri archiveUri, int accessMode) { return DocumentsContract.buildDocumentUri(AUTHORITY, new ArchiveId(archiveUri, accessMode, "/").toDocumentId()); } /** Loading Loading @@ -263,8 +269,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException { private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException { final ArchiveId id = ArchiveId.fromDocumentId(documentId); if (mArchives.get(id.mArchiveUri) != null) { return mArchives.get(id.mArchiveUri); Loading @@ -281,7 +286,8 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { Document.COLUMN_MIME_TYPE)); Preconditions.checkArgument(isSupportedArchiveType(mimeType)); final Uri notificationUri = cursor.getNotificationUri(); final Loader loader = new Loader(getContext(), id.mArchiveUri, notificationUri); final Loader loader = new Loader(getContext(), id.mArchiveUri, id.mAccessMode, notificationUri); // Remove the instance from mArchives collection once the archive file changes. if (notificationUri != null) { Loading src/com/android/documentsui/archives/Loader.java +15 −7 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ public class Loader { private final Context mContext; private final Uri mArchiveUri; private final int mAccessMode; private final Uri mNotificationUri; private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); Loading @@ -50,9 +51,10 @@ public class Loader { private int mStatus = STATUS_OPENING; private Archive mArchive = null; Loader(Context context, Uri archiveUri, Uri notificationUri) { Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { this.mContext = context; this.mArchiveUri = archiveUri; this.mAccessMode = accessMode; this.mNotificationUri = notificationUri; // Start loading the archive immediately in the background. Loading @@ -77,11 +79,17 @@ public class Loader { } try { mArchive = Archive.createForParcelFileDescriptor( if (ReadableArchive.supportsAccessMode(mAccessMode)) { mArchive = ReadableArchive.createForParcelFileDescriptor( mContext, mContext.getContentResolver().openFileDescriptor( mArchiveUri, "r", null /* signal */), mArchiveUri, mNotificationUri); mArchiveUri, mAccessMode, mNotificationUri); // TODO: // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { } else { throw new IllegalStateException("Access mode not supported."); } synchronized (mStatusLock) { mStatus = STATUS_OPENED; } Loading @@ -95,7 +103,7 @@ public class Loader { // Notify observers that the root directory is loaded (or failed) // so clients reload it. mContext.getContentResolver().notifyChange( ArchivesProvider.buildUriForArchive(mArchiveUri), ArchivesProvider.buildUriForArchive(mArchiveUri, mAccessMode), null /* observer */, false /* syncToNetwork */); } Loading Loading
src/com/android/documentsui/DocumentsAccess.java +3 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Path; Loading Loading @@ -107,7 +108,8 @@ public interface DocumentsAccess { @Override public DocumentInfo getArchiveDocument(Uri uri) { return getDocument(ArchivesProvider.buildUriForArchive(uri)); return getDocument(ArchivesProvider.buildUriForArchive(uri, ParcelFileDescriptor.MODE_READ_ONLY)); } @Override Loading
src/com/android/documentsui/archives/Archive.java +25 −285 Original line number Diff line number Diff line Loading @@ -21,44 +21,26 @@ import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Point; import android.media.ExifInterface; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; import android.util.Log; import android.util.jar.StrictJarFile; import android.webkit.MimeTypeMap; import com.android.internal.util.Preconditions; import libcore.io.IoUtils; import java.io.Closeable; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Stack; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; Loading @@ -70,7 +52,7 @@ import java.util.zip.ZipEntry; * * <p>This class is thread safe. */ public class Archive implements Closeable { public abstract class Archive implements Closeable { private static final String TAG = "Archive"; public static final String[] DEFAULT_PROJECTION = new String[] { Loading @@ -81,27 +63,23 @@ public class Archive implements Closeable { Document.COLUMN_FLAGS }; private final Context mContext; private final Uri mArchiveUri; private final Uri mNotificationUri; private final StrictJarFile mZipFile; private final ThreadPoolExecutor mExecutor; private final Map<String, ZipEntry> mEntries; private final Map<String, List<ZipEntry>> mTree; final Context mContext; final Uri mArchiveUri; final int mArchiveMode; final Uri mNotificationUri; final ThreadPoolExecutor mExecutor; final Map<String, ZipEntry> mEntries; final Map<String, List<ZipEntry>> mTree; private Archive( Archive( Context context, @Nullable File file, @Nullable FileDescriptor fd, Uri archiveUri, @Nullable Uri notificationUri) throws IOException { int archiveMode, @Nullable Uri notificationUri) { mContext = context; mArchiveUri = archiveUri; mArchiveMode = archiveMode; mNotificationUri = notificationUri; mZipFile = file != null ? new StrictJarFile(file.getPath(), false /* verify */, false /* signatures */) : new StrictJarFile(fd, false /* verify */, false /* signatures */); // At most 8 active threads. All threads idling for more than a minute will // be closed. Loading @@ -109,68 +87,8 @@ public class Archive implements Closeable { new LinkedBlockingQueue<Runnable>()); mExecutor.allowCoreThreadTimeOut(true); // Build the tree structure in memory. mTree = new HashMap<>(); mEntries = new HashMap<>(); ZipEntry entry; String entryPath; final Iterator<ZipEntry> it = mZipFile.iterator(); final Stack<ZipEntry> stack = new Stack<>(); while (it.hasNext()) { entry = it.next(); if (entry.isDirectory() != entry.getName().endsWith("/")) { throw new IOException( "Directories must have a trailing slash, and files must not."); } entryPath = getEntryPath(entry); if (mEntries.containsKey(entryPath)) { throw new IOException("Multiple entries with the same name are not supported."); } mEntries.put(entryPath, entry); if (entry.isDirectory()) { mTree.put(entryPath, new ArrayList<ZipEntry>()); } if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent. stack.push(entry); } } int delimiterIndex; String parentPath; ZipEntry parentEntry; List<ZipEntry> parentList; // Go through all directories recursively and build a tree structure. while (stack.size() > 0) { entry = stack.pop(); entryPath = getEntryPath(entry); delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory() ? entryPath.length() - 2 : entryPath.length() - 1); parentPath = entryPath.substring(0, delimiterIndex) + "/"; parentList = mTree.get(parentPath); if (parentList == null) { // The ZIP file doesn't contain all directories leading to the entry. // It's rare, but can happen in a valid ZIP archive. In such case create a // fake ZipEntry and add it on top of the stack to process it next. parentEntry = new ZipEntry(parentPath); parentEntry.setSize(0); parentEntry.setTime(entry.getTime()); mEntries.put(parentPath, parentEntry); if (!"/".equals(parentPath)) { stack.push(parentEntry); } parentList = new ArrayList<>(); mTree.put(parentPath, parentList); } parentList.add(entry); } } /** Loading @@ -190,7 +108,6 @@ public class Archive implements Closeable { * Returns true if the file descriptor is seekable. * @param descriptor File descriptor to check. */ @VisibleForTesting public static boolean canSeek(ParcelFileDescriptor descriptor) { try { return Os.lseek(descriptor.getFileDescriptor(), 0, Loading @@ -200,75 +117,6 @@ public class Archive implements Closeable { } } /** * Creates a DocumentsArchive instance for opening, browsing and accessing * documents within the archive passed as a file descriptor. * * If the file descriptor is not seekable, then a snapshot will be created. * * This method takes ownership for the passed descriptor. The caller must * not close it. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. * @param archiveUri Uri of the archive document. * @param Uri notificationUri Uri for notifying that the archive file has changed. */ public static Archive createForParcelFileDescriptor( Context context, ParcelFileDescriptor descriptor, Uri archiveUri, @Nullable Uri notificationUri) throws IOException { FileDescriptor fd = null; try { if (canSeek(descriptor)) { fd = new FileDescriptor(); fd.setInt$(descriptor.detachFd()); return new Archive(context, null, fd, archiveUri, notificationUri); } // Fallback for non-seekable file descriptors. File snapshotFile = null; try { // Create a copy of the archive, as ZipFile doesn't operate on streams. // Moreover, ZipInputStream would be inefficient for large files on // pipes. snapshotFile = File.createTempFile("com.android.documentsui.snapshot{", "}.zip", context.getCacheDir()); try ( final FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( ParcelFileDescriptor.open( snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY)); final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor); ) { final byte[] buffer = new byte[32 * 1024]; int bytes; while ((bytes = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytes); } outputStream.flush(); } return new Archive(context, snapshotFile, null, archiveUri, notificationUri); } finally { // On UNIX the file will be still available for processes which opened it, even // after deleting it. Remove it ASAP, as it won't be used by anyone else. if (snapshotFile != null) { snapshotFile.delete(); } } } catch (Exception e) { // Since the method takes ownership of the passed descriptor, close it // on exception. IoUtils.closeQuietly(descriptor); IoUtils.closeQuietly(fd); throw e; } } /** * Lists child documents of an archive or a directory within an * archive. Must be called only for archives with supported mime type, Loading Loading @@ -376,127 +224,24 @@ public class Archive implements Closeable { * * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) */ public ParcelFileDescriptor openDocument( abstract public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { MorePreconditions.checkArgumentEquals("r", mode, "Invalid mode. Only reading \"r\" supported, but got: \"%s\"."); final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } ParcelFileDescriptor[] pipe; InputStream inputStream = null; try { pipe = ParcelFileDescriptor.createReliablePipe(); inputStream = mZipFile.getInputStream(entry); } catch (IOException e) { if (inputStream != null) { IoUtils.closeQuietly(inputStream); } // Ideally we'd simply throw IOException to the caller, but for consistency // with DocumentsProvider::openDocument, converting it to IllegalStateException. throw new IllegalStateException("Failed to open the document.", e); } final ParcelFileDescriptor outputPipe = pipe[1]; final InputStream finalInputStream = inputStream; mExecutor.execute( new Runnable() { @Override public void run() { try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { try { final byte buffer[] = new byte[32 * 1024]; int bytes; while ((bytes = finalInputStream.read(buffer)) != -1) { if (Thread.interrupted()) { throw new InterruptedException(); } if (signal != null) { signal.throwIfCanceled(); } outputStream.write(buffer, 0, bytes); } } catch (IOException | InterruptedException e) { // Catch the exception before the outer try-with-resource closes the // pipe with close() instead of closeWithError(). try { outputPipe.closeWithError(e.getMessage()); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { Log.e(TAG, "Failed to close the output stream gracefully.", e); } finally { IoUtils.closeQuietly(finalInputStream); } } }); return pipe[0]; } throws FileNotFoundException; /** * Opens a thumbnail of a file within an archive. * * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) */ public AssetFileDescriptor openDocumentThumbnail( abstract public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException { final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"), "Thumbnails only supported for image/* MIME type."); final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } throws FileNotFoundException; InputStream inputStream = null; try { inputStream = mZipFile.getInputStream(entry); final ExifInterface exif = new ExifInterface(inputStream); if (exif.hasThumbnail()) { Bundle extras = null; switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) { case ExifInterface.ORIENTATION_ROTATE_90: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90); break; case ExifInterface.ORIENTATION_ROTATE_180: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180); break; case ExifInterface.ORIENTATION_ROTATE_270: extras = new Bundle(1); extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270); break; } final long[] range = exif.getThumbnailRange(); return new AssetFileDescriptor( openDocument(documentId, "r", signal), range[0], range[1], extras); } } catch (IOException e) { // Ignore the exception, as reading the EXIF may legally fail. Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e); } finally { IoUtils.closeQuietly(inputStream); } return new AssetFileDescriptor( openDocument(documentId, "r", signal), 0, entry.getSize(), null); /** * Creates an archive id for the passed path. */ public ArchiveId createArchiveId(String path) { return new ArchiveId(mArchiveUri, mArchiveMode, path); } /** Loading @@ -508,16 +253,11 @@ public class Archive implements Closeable { @Override public void close() { mExecutor.shutdownNow(); try { mZipFile.close(); } catch (IOException e) { // Silent close. } } private void addCursorRow(MatrixCursor cursor, ZipEntry entry) { void addCursorRow(MatrixCursor cursor, ZipEntry entry) { final MatrixCursor.RowBuilder row = cursor.newRow(); final ArchiveId parsedId = new ArchiveId(mArchiveUri, getEntryPath(entry)); final ArchiveId parsedId = createArchiveId(getEntryPath(entry)); row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId()); final File file = new File(entry.getName()); Loading @@ -531,7 +271,7 @@ public class Archive implements Closeable { row.add(Document.COLUMN_FLAGS, flags); } private String getMimeTypeForEntry(ZipEntry entry) { static String getMimeTypeForEntry(ZipEntry entry) { if (entry.isDirectory()) { return Document.MIME_TYPE_DIR; } Loading @@ -549,7 +289,7 @@ public class Archive implements Closeable { } // TODO: Upstream to the Preconditions class. private static class MorePreconditions { static class MorePreconditions { static void checkArgumentEquals(String expected, @Nullable String actual, String message) { if (!TextUtils.equals(expected, actual)) { Loading
src/com/android/documentsui/archives/ArchiveId.java +19 −5 Original line number Diff line number Diff line Loading @@ -22,22 +22,36 @@ public class ArchiveId { private final static char DELIMITER = '#'; public final Uri mArchiveUri; public final int mAccessMode; public final String mPath; public ArchiveId(Uri archiveUri, String path) { public ArchiveId(Uri archiveUri, int accessMode, String path) { assert(archiveUri.toString().indexOf(DELIMITER) == -1); assert(!path.isEmpty()); mArchiveUri = archiveUri; mAccessMode = accessMode; mPath = path; assert(!mPath.isEmpty()); } static public ArchiveId fromDocumentId(String documentId) { final int delimiterPosition = documentId.indexOf(DELIMITER); assert(delimiterPosition != -1); return new ArchiveId(Uri.parse(documentId.substring(0, delimiterPosition)), documentId.substring((delimiterPosition + 1))); final int secondDelimiterPosition = documentId.indexOf(DELIMITER, delimiterPosition + 1); assert(secondDelimiterPosition != -1); final String archiveUriPart = documentId.substring(0, delimiterPosition); final String accessModePart = documentId.substring(delimiterPosition + 1, secondDelimiterPosition); final String pathPart = documentId.substring(secondDelimiterPosition + 1); return new ArchiveId(Uri.parse(archiveUriPart), Integer.parseInt(accessModePart), pathPart); } public String toDocumentId() { return mArchiveUri.toString() + DELIMITER + mPath; return mArchiveUri.toString() + DELIMITER + mAccessMode + DELIMITER + mPath; } };
src/com/android/documentsui/archives/ArchivesProvider.java +13 −7 Original line number Diff line number Diff line Loading @@ -121,7 +121,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { cursor.setExtras(bundle); cursor.setNotificationUri(getContext().getContentResolver(), buildUriForArchive(archiveId.mArchiveUri)); buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); return cursor; } finally { releaseInstance(loader); Loading Loading @@ -231,9 +231,15 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { return false; } public static Uri buildUriForArchive(Uri archiveUri) { return DocumentsContract.buildDocumentUri( AUTHORITY, new ArchiveId(archiveUri, "/").toDocumentId()); /** * Creates a Uri for accessing an archive with the specified access mode. * * @see ParcelFileDescriptor#MODE_READ * @see ParcelFileDescriptor#MODE_WRITE */ public static Uri buildUriForArchive(Uri archiveUri, int accessMode) { return DocumentsContract.buildDocumentUri(AUTHORITY, new ArchiveId(archiveUri, accessMode, "/").toDocumentId()); } /** Loading Loading @@ -263,8 +269,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException { private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException { final ArchiveId id = ArchiveId.fromDocumentId(documentId); if (mArchives.get(id.mArchiveUri) != null) { return mArchives.get(id.mArchiveUri); Loading @@ -281,7 +286,8 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { Document.COLUMN_MIME_TYPE)); Preconditions.checkArgument(isSupportedArchiveType(mimeType)); final Uri notificationUri = cursor.getNotificationUri(); final Loader loader = new Loader(getContext(), id.mArchiveUri, notificationUri); final Loader loader = new Loader(getContext(), id.mArchiveUri, id.mAccessMode, notificationUri); // Remove the instance from mArchives collection once the archive file changes. if (notificationUri != null) { Loading
src/com/android/documentsui/archives/Loader.java +15 −7 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ public class Loader { private final Context mContext; private final Uri mArchiveUri; private final int mAccessMode; private final Uri mNotificationUri; private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); Loading @@ -50,9 +51,10 @@ public class Loader { private int mStatus = STATUS_OPENING; private Archive mArchive = null; Loader(Context context, Uri archiveUri, Uri notificationUri) { Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { this.mContext = context; this.mArchiveUri = archiveUri; this.mAccessMode = accessMode; this.mNotificationUri = notificationUri; // Start loading the archive immediately in the background. Loading @@ -77,11 +79,17 @@ public class Loader { } try { mArchive = Archive.createForParcelFileDescriptor( if (ReadableArchive.supportsAccessMode(mAccessMode)) { mArchive = ReadableArchive.createForParcelFileDescriptor( mContext, mContext.getContentResolver().openFileDescriptor( mArchiveUri, "r", null /* signal */), mArchiveUri, mNotificationUri); mArchiveUri, mAccessMode, mNotificationUri); // TODO: // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { } else { throw new IllegalStateException("Access mode not supported."); } synchronized (mStatusLock) { mStatus = STATUS_OPENED; } Loading @@ -95,7 +103,7 @@ public class Loader { // Notify observers that the root directory is loaded (or failed) // so clients reload it. mContext.getContentResolver().notifyChange( ArchivesProvider.buildUriForArchive(mArchiveUri), ArchivesProvider.buildUriForArchive(mArchiveUri, mAccessMode), null /* observer */, false /* syncToNetwork */); } Loading