Loading src/com/android/documentsui/archives/Archive.java +77 −42 Original line number Diff line number Diff line Loading @@ -32,8 +32,11 @@ import android.system.OsConstants; import android.text.TextUtils; import android.webkit.MimeTypeMap; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import android.support.annotation.VisibleForTesting; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; Loading Loading @@ -63,19 +66,25 @@ public abstract class Archive implements Closeable { final Context mContext; final Uri mArchiveUri; final int mArchiveMode; final int mAccessMode; final Uri mNotificationUri; // The container as well as values are guarded by mEntries. @GuardedBy("mEntries") final Map<String, ZipEntry> mEntries; // The container as well as values and elements of values are guarded by mEntries. @GuardedBy("mEntries") final Map<String, List<ZipEntry>> mTree; Archive( Context context, Uri archiveUri, int archiveMode, int accessMode, @Nullable Uri notificationUri) { mContext = context; mArchiveUri = archiveUri; mArchiveMode = archiveMode; mAccessMode = accessMode; mNotificationUri = notificationUri; mTree = new HashMap<>(); Loading Loading @@ -127,6 +136,7 @@ public abstract class Archive implements Closeable { result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); } synchronized (mEntries) { final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath); if (parentList == null) { throw new FileNotFoundException(); Loading @@ -134,6 +144,7 @@ public abstract class Archive implements Closeable { for (final ZipEntry entry : parentList) { addCursorRow(result, entry); } } return result; } Loading @@ -147,12 +158,14 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } return getMimeTypeForEntry(entry); } } /** * Returns true if a document within an archive is a child or any descendant of the archive Loading @@ -166,6 +179,7 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { return false; Loading @@ -184,6 +198,7 @@ public abstract class Archive implements Closeable { return pathWithSlash.startsWith(parsedParentId.mPath) && !parsedParentId.mPath.equals(pathWithSlash); } } /** * Returns metadata of a document within an archive. Loading @@ -196,6 +211,7 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); Loading @@ -209,32 +225,51 @@ public abstract class Archive implements Closeable { addCursorRow(result, entry); return result; } } /** * Creates a file within an archive. * * @see DocumentsProvider.createDocument(String, String, String)) */ @VisibleForTesting public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { throw new UnsupportedOperationException("Creating documents not supported."); } /** * Opens a file within an archive. * * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) */ abstract public ParcelFileDescriptor openDocument( public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException; throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); } /** * Opens a thumbnail of a file within an archive. * * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) */ abstract public AssetFileDescriptor openDocumentThumbnail( public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException; throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); } /** * Creates an archive id for the passed path. */ public ArchiveId createArchiveId(String path) { return new ArchiveId(mArchiveUri, mArchiveMode, path); return new ArchiveId(mArchiveUri, mAccessMode, path); } /** * Not thread safe. */ void addCursorRow(MatrixCursor cursor, ZipEntry entry) { final MatrixCursor.RowBuilder row = cursor.newRow(); final ArchiveId parsedId = createArchiveId(getEntryPath(entry)); Loading src/com/android/documentsui/archives/Loader.java +6 −2 Original line number Diff line number Diff line Loading @@ -85,8 +85,12 @@ public class Loader { mContext.getContentResolver().openFileDescriptor( mArchiveUri, "r", null /* signal */), mArchiveUri, mAccessMode, mNotificationUri); // TODO: // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { mArchive = WriteableArchive.createForParcelFileDescriptor( mContext, mContext.getContentResolver().openFileDescriptor( mArchiveUri, "w", null /* signal */), mArchiveUri, mAccessMode, mNotificationUri); } else { throw new IllegalStateException("Access mode not supported."); } Loading src/com/android/documentsui/archives/ReadableArchive.java +3 −2 Original line number Diff line number Diff line Loading @@ -165,7 +165,7 @@ public class ReadableArchive extends Archive { * 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. * not use it after passing. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. Loading Loading @@ -284,7 +284,8 @@ public class ReadableArchive extends Archive { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { outputPipe.closeWithError(e.getMessage()); Log.e(TAG, "Failed while reading a file.", e); outputPipe.closeWithError("Reading failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } Loading src/com/android/documentsui/archives/WriteableArchive.java 0 → 100644 +311 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.archives; import android.content.Context; import android.net.Uri; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.util.Log; import com.android.internal.annotations.GuardedBy; import android.support.annotation.VisibleForTesting; import libcore.io.IoUtils; 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.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Provides basic implementation for creating archives. * * <p>This class is thread safe. */ public class WriteableArchive extends Archive { private static final String TAG = "WriteableArchive"; @GuardedBy("mEntries") private final Set<String> mPendingEntries = new HashSet<>(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); @GuardedBy("mEntries") private final ZipOutputStream mZipOutputStream; private WriteableArchive( Context context, FileDescriptor fd, Uri archiveUri, int accessMode, @Nullable Uri notificationUri) throws IOException { super(context, archiveUri, accessMode, notificationUri); if (!supportsAccessMode(accessMode)) { throw new IllegalStateException("Unsupported access mode."); } addEntry(null /* no parent */, new ZipEntry("/")); // Root entry. mZipOutputStream = new ZipOutputStream(new FileOutputStream(fd)); } private void addEntry(@Nullable ZipEntry parentEntry, ZipEntry entry) { final String entryPath = getEntryPath(entry); synchronized (mEntries) { if (entry.isDirectory()) { if (!mTree.containsKey(entryPath)) { mTree.put(entryPath, new ArrayList<ZipEntry>()); } } mEntries.put(entryPath, entry); if (parentEntry != null) { mTree.get(getEntryPath(parentEntry)).add(entry); } } } /** * @see ParcelFileDescriptor */ public static boolean supportsAccessMode(int accessMode) { return accessMode == ParcelFileDescriptor.MODE_WRITE_ONLY; } /** * Creates a DocumentsArchive instance for writing into an archive file passed * as a file descriptor. * * This method takes ownership for the passed descriptor. The caller must * not use it after passing. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. * @param archiveUri Uri of the archive document. * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}. * @param Uri notificationUri Uri for notifying that the archive file has changed. */ @VisibleForTesting public static WriteableArchive createForParcelFileDescriptor( Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode, @Nullable Uri notificationUri) throws IOException { FileDescriptor fd = null; try { fd = new FileDescriptor(); fd.setInt$(descriptor.detachFd()); return new WriteableArchive(context, fd, archiveUri, accessMode, notificationUri); } catch (Exception e) { // Since the method takes ownership of the passed descriptor, close it // on exception. IoUtils.closeQuietly(descriptor); IoUtils.closeQuietly(fd); throw e; } } @Override @VisibleForTesting public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); final boolean isDirectory = Document.MIME_TYPE_DIR.equals(mimeType); ZipEntry entry; String entryPath; synchronized (mEntries) { final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath); if (parentEntry == null) { throw new FileNotFoundException(); } if (displayName.indexOf("/") != -1 || ".".equals(displayName) || "..".equals(displayName)) { throw new IllegalStateException("Display name contains invalid characters."); } if ("".equals(displayName)) { throw new IllegalStateException("Display name cannot be empty."); } assert(parentEntry.getName().endsWith("/")); final String parentName = "/".equals(parentEntry.getName()) ? "" : parentEntry.getName(); final String entryName = parentName + displayName + (isDirectory ? "/" : ""); entry = new ZipEntry(entryName); entryPath = getEntryPath(entry); entry.setSize(0); if (mEntries.get(entryPath) != null) { throw new IllegalStateException("The document already exist: " + entryPath); } addEntry(parentEntry, entry); } if (!isDirectory) { // For files, the contents will be written via openDocument. Since the contents // must be immediately followed by the contents, defer adding the header until // openDocument. All pending entires which haven't been written will be added // to the ZIP file in close(). synchronized (mEntries) { mPendingEntries.add(entryPath); } } else { try { synchronized (mEntries) { mZipOutputStream.putNextEntry(entry); } } catch (IOException e) { throw new IllegalStateException( "Failed to create a file in the archive: " + entryPath, e); } } return createArchiveId(entryPath).toDocumentId(); } @Override public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { MorePreconditions.checkArgumentEquals("w", mode, "Invalid mode. Only writing \"w\" 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; synchronized (mEntries) { entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } if (!mPendingEntries.contains(parsedId.mPath)) { throw new IllegalStateException("Files can be written only once."); } mPendingEntries.remove(parsedId.mPath); } ParcelFileDescriptor[] pipe; try { pipe = ParcelFileDescriptor.createReliablePipe(); } catch (IOException e) { // 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 inputPipe = pipe[0]; try { mExecutor.execute( new Runnable() { @Override public void run() { try (final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputPipe)) { try { synchronized (mEntries) { mZipOutputStream.putNextEntry(entry); final byte buffer[] = new byte[32 * 1024]; int bytes; long size = 0; while ((bytes = inputStream.read(buffer)) != -1) { if (signal != null) { signal.throwIfCanceled(); } mZipOutputStream.write(buffer, 0, bytes); size += bytes; } entry.setSize(size); mZipOutputStream.closeEntry(); } } catch (IOException e) { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { Log.e(TAG, "Failed while writing to a file.", e); inputPipe.closeWithError("Writing failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { // Input stream auto-close error. Close quietly. } } }); } catch (RejectedExecutionException e) { IoUtils.closeQuietly(pipe[0]); IoUtils.closeQuietly(pipe[1]); throw new IllegalStateException("Failed to initialize pipe."); } return pipe[1]; } /** * Closes the archive. Blocks until all enqueued pipes are completed. */ @Override public void close() { // Waits until all enqueued pipe requests are completed. mExecutor.shutdown(); try { final boolean result = mExecutor.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS); assert(result); } catch (InterruptedException e) { Log.e(TAG, "Opened files failed to be fullly written.", e); } // Flush all pending entries. They will all have empty size. synchronized (mEntries) { for (final String path : mPendingEntries) { try { mZipOutputStream.putNextEntry(mEntries.get(path)); mZipOutputStream.closeEntry(); } catch (IOException e) { Log.e(TAG, "Failed to flush empty entries.", e); } } try { mZipOutputStream.close(); } catch (IOException e) { Log.e(TAG, "Failed while closing the ZIP file.", e); } } } }; tests/unit/com/android/documentsui/archives/TestUtils.java +8 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,14 @@ public class TestUtils { mExecutor = executor; } /** * Creates an empty temporary file. */ public File createTemporaryFile() throws IOException { return File.createTempFile("com.android.documentsui.archives.tests{", "}.zip", mTargetContext.getCacheDir()); } /** * Opens a resource and returns the contents via file descriptor to a local * snapshot file. Loading Loading
src/com/android/documentsui/archives/Archive.java +77 −42 Original line number Diff line number Diff line Loading @@ -32,8 +32,11 @@ import android.system.OsConstants; import android.text.TextUtils; import android.webkit.MimeTypeMap; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import android.support.annotation.VisibleForTesting; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; Loading Loading @@ -63,19 +66,25 @@ public abstract class Archive implements Closeable { final Context mContext; final Uri mArchiveUri; final int mArchiveMode; final int mAccessMode; final Uri mNotificationUri; // The container as well as values are guarded by mEntries. @GuardedBy("mEntries") final Map<String, ZipEntry> mEntries; // The container as well as values and elements of values are guarded by mEntries. @GuardedBy("mEntries") final Map<String, List<ZipEntry>> mTree; Archive( Context context, Uri archiveUri, int archiveMode, int accessMode, @Nullable Uri notificationUri) { mContext = context; mArchiveUri = archiveUri; mArchiveMode = archiveMode; mAccessMode = accessMode; mNotificationUri = notificationUri; mTree = new HashMap<>(); Loading Loading @@ -127,6 +136,7 @@ public abstract class Archive implements Closeable { result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); } synchronized (mEntries) { final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath); if (parentList == null) { throw new FileNotFoundException(); Loading @@ -134,6 +144,7 @@ public abstract class Archive implements Closeable { for (final ZipEntry entry : parentList) { addCursorRow(result, entry); } } return result; } Loading @@ -147,12 +158,14 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } return getMimeTypeForEntry(entry); } } /** * Returns true if a document within an archive is a child or any descendant of the archive Loading @@ -166,6 +179,7 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { return false; Loading @@ -184,6 +198,7 @@ public abstract class Archive implements Closeable { return pathWithSlash.startsWith(parsedParentId.mPath) && !parsedParentId.mPath.equals(pathWithSlash); } } /** * Returns metadata of a document within an archive. Loading @@ -196,6 +211,7 @@ public abstract class Archive implements Closeable { MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); synchronized (mEntries) { final ZipEntry entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); Loading @@ -209,32 +225,51 @@ public abstract class Archive implements Closeable { addCursorRow(result, entry); return result; } } /** * Creates a file within an archive. * * @see DocumentsProvider.createDocument(String, String, String)) */ @VisibleForTesting public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { throw new UnsupportedOperationException("Creating documents not supported."); } /** * Opens a file within an archive. * * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) */ abstract public ParcelFileDescriptor openDocument( public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException; throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); } /** * Opens a thumbnail of a file within an archive. * * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) */ abstract public AssetFileDescriptor openDocumentThumbnail( public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException; throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); } /** * Creates an archive id for the passed path. */ public ArchiveId createArchiveId(String path) { return new ArchiveId(mArchiveUri, mArchiveMode, path); return new ArchiveId(mArchiveUri, mAccessMode, path); } /** * Not thread safe. */ void addCursorRow(MatrixCursor cursor, ZipEntry entry) { final MatrixCursor.RowBuilder row = cursor.newRow(); final ArchiveId parsedId = createArchiveId(getEntryPath(entry)); Loading
src/com/android/documentsui/archives/Loader.java +6 −2 Original line number Diff line number Diff line Loading @@ -85,8 +85,12 @@ public class Loader { mContext.getContentResolver().openFileDescriptor( mArchiveUri, "r", null /* signal */), mArchiveUri, mAccessMode, mNotificationUri); // TODO: // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { } else if (WriteableArchive.supportsAccessMode(mAccessMode)) { mArchive = WriteableArchive.createForParcelFileDescriptor( mContext, mContext.getContentResolver().openFileDescriptor( mArchiveUri, "w", null /* signal */), mArchiveUri, mAccessMode, mNotificationUri); } else { throw new IllegalStateException("Access mode not supported."); } Loading
src/com/android/documentsui/archives/ReadableArchive.java +3 −2 Original line number Diff line number Diff line Loading @@ -165,7 +165,7 @@ public class ReadableArchive extends Archive { * 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. * not use it after passing. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. Loading Loading @@ -284,7 +284,8 @@ public class ReadableArchive extends Archive { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { outputPipe.closeWithError(e.getMessage()); Log.e(TAG, "Failed while reading a file.", e); outputPipe.closeWithError("Reading failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } Loading
src/com/android/documentsui/archives/WriteableArchive.java 0 → 100644 +311 −0 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.archives; import android.content.Context; import android.net.Uri; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.util.Log; import com.android.internal.annotations.GuardedBy; import android.support.annotation.VisibleForTesting; import libcore.io.IoUtils; 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.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Provides basic implementation for creating archives. * * <p>This class is thread safe. */ public class WriteableArchive extends Archive { private static final String TAG = "WriteableArchive"; @GuardedBy("mEntries") private final Set<String> mPendingEntries = new HashSet<>(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); @GuardedBy("mEntries") private final ZipOutputStream mZipOutputStream; private WriteableArchive( Context context, FileDescriptor fd, Uri archiveUri, int accessMode, @Nullable Uri notificationUri) throws IOException { super(context, archiveUri, accessMode, notificationUri); if (!supportsAccessMode(accessMode)) { throw new IllegalStateException("Unsupported access mode."); } addEntry(null /* no parent */, new ZipEntry("/")); // Root entry. mZipOutputStream = new ZipOutputStream(new FileOutputStream(fd)); } private void addEntry(@Nullable ZipEntry parentEntry, ZipEntry entry) { final String entryPath = getEntryPath(entry); synchronized (mEntries) { if (entry.isDirectory()) { if (!mTree.containsKey(entryPath)) { mTree.put(entryPath, new ArrayList<ZipEntry>()); } } mEntries.put(entryPath, entry); if (parentEntry != null) { mTree.get(getEntryPath(parentEntry)).add(entry); } } } /** * @see ParcelFileDescriptor */ public static boolean supportsAccessMode(int accessMode) { return accessMode == ParcelFileDescriptor.MODE_WRITE_ONLY; } /** * Creates a DocumentsArchive instance for writing into an archive file passed * as a file descriptor. * * This method takes ownership for the passed descriptor. The caller must * not use it after passing. * * @param context Context of the provider. * @param descriptor File descriptor for the archive's contents. * @param archiveUri Uri of the archive document. * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}. * @param Uri notificationUri Uri for notifying that the archive file has changed. */ @VisibleForTesting public static WriteableArchive createForParcelFileDescriptor( Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode, @Nullable Uri notificationUri) throws IOException { FileDescriptor fd = null; try { fd = new FileDescriptor(); fd.setInt$(descriptor.detachFd()); return new WriteableArchive(context, fd, archiveUri, accessMode, notificationUri); } catch (Exception e) { // Since the method takes ownership of the passed descriptor, close it // on exception. IoUtils.closeQuietly(descriptor); IoUtils.closeQuietly(fd); throw e; } } @Override @VisibleForTesting public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId); MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, "Mismatching archive Uri. Expected: %s, actual: %s."); final boolean isDirectory = Document.MIME_TYPE_DIR.equals(mimeType); ZipEntry entry; String entryPath; synchronized (mEntries) { final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath); if (parentEntry == null) { throw new FileNotFoundException(); } if (displayName.indexOf("/") != -1 || ".".equals(displayName) || "..".equals(displayName)) { throw new IllegalStateException("Display name contains invalid characters."); } if ("".equals(displayName)) { throw new IllegalStateException("Display name cannot be empty."); } assert(parentEntry.getName().endsWith("/")); final String parentName = "/".equals(parentEntry.getName()) ? "" : parentEntry.getName(); final String entryName = parentName + displayName + (isDirectory ? "/" : ""); entry = new ZipEntry(entryName); entryPath = getEntryPath(entry); entry.setSize(0); if (mEntries.get(entryPath) != null) { throw new IllegalStateException("The document already exist: " + entryPath); } addEntry(parentEntry, entry); } if (!isDirectory) { // For files, the contents will be written via openDocument. Since the contents // must be immediately followed by the contents, defer adding the header until // openDocument. All pending entires which haven't been written will be added // to the ZIP file in close(). synchronized (mEntries) { mPendingEntries.add(entryPath); } } else { try { synchronized (mEntries) { mZipOutputStream.putNextEntry(entry); } } catch (IOException e) { throw new IllegalStateException( "Failed to create a file in the archive: " + entryPath, e); } } return createArchiveId(entryPath).toDocumentId(); } @Override public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { MorePreconditions.checkArgumentEquals("w", mode, "Invalid mode. Only writing \"w\" 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; synchronized (mEntries) { entry = mEntries.get(parsedId.mPath); if (entry == null) { throw new FileNotFoundException(); } if (!mPendingEntries.contains(parsedId.mPath)) { throw new IllegalStateException("Files can be written only once."); } mPendingEntries.remove(parsedId.mPath); } ParcelFileDescriptor[] pipe; try { pipe = ParcelFileDescriptor.createReliablePipe(); } catch (IOException e) { // 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 inputPipe = pipe[0]; try { mExecutor.execute( new Runnable() { @Override public void run() { try (final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputPipe)) { try { synchronized (mEntries) { mZipOutputStream.putNextEntry(entry); final byte buffer[] = new byte[32 * 1024]; int bytes; long size = 0; while ((bytes = inputStream.read(buffer)) != -1) { if (signal != null) { signal.throwIfCanceled(); } mZipOutputStream.write(buffer, 0, bytes); size += bytes; } entry.setSize(size); mZipOutputStream.closeEntry(); } } catch (IOException e) { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { Log.e(TAG, "Failed while writing to a file.", e); inputPipe.closeWithError("Writing failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { // Input stream auto-close error. Close quietly. } } }); } catch (RejectedExecutionException e) { IoUtils.closeQuietly(pipe[0]); IoUtils.closeQuietly(pipe[1]); throw new IllegalStateException("Failed to initialize pipe."); } return pipe[1]; } /** * Closes the archive. Blocks until all enqueued pipes are completed. */ @Override public void close() { // Waits until all enqueued pipe requests are completed. mExecutor.shutdown(); try { final boolean result = mExecutor.awaitTermination( Long.MAX_VALUE, TimeUnit.MILLISECONDS); assert(result); } catch (InterruptedException e) { Log.e(TAG, "Opened files failed to be fullly written.", e); } // Flush all pending entries. They will all have empty size. synchronized (mEntries) { for (final String path : mPendingEntries) { try { mZipOutputStream.putNextEntry(mEntries.get(path)); mZipOutputStream.closeEntry(); } catch (IOException e) { Log.e(TAG, "Failed to flush empty entries.", e); } } try { mZipOutputStream.close(); } catch (IOException e) { Log.e(TAG, "Failed while closing the ZIP file.", e); } } } };
tests/unit/com/android/documentsui/archives/TestUtils.java +8 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,14 @@ public class TestUtils { mExecutor = executor; } /** * Creates an empty temporary file. */ public File createTemporaryFile() throws IOException { return File.createTempFile("com.android.documentsui.archives.tests{", "}.zip", mTargetContext.getCacheDir()); } /** * Opens a resource and returns the contents via file descriptor to a local * snapshot file. Loading