Merge "Refactor archives to support creating archives." into arc-apps
diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java
index 63f235c..5d09aa6 100644
--- a/src/com/android/documentsui/DocumentsAccess.java
+++ b/src/com/android/documentsui/DocumentsAccess.java
@@ -22,6 +22,7 @@
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;
@@ -107,7 +108,8 @@
@Override
public DocumentInfo getArchiveDocument(Uri uri) {
- return getDocument(ArchivesProvider.buildUriForArchive(uri));
+ return getDocument(ArchivesProvider.buildUriForArchive(uri,
+ ParcelFileDescriptor.MODE_READ_ONLY));
}
@Override
diff --git a/src/com/android/documentsui/archives/Archive.java b/src/com/android/documentsui/archives/Archive.java
index 496ffdc..8e889ff 100644
--- a/src/com/android/documentsui/archives/Archive.java
+++ b/src/com/android/documentsui/archives/Archive.java
@@ -21,44 +21,26 @@
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;
@@ -70,7 +52,7 @@
*
* <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[] {
@@ -81,27 +63,23 @@
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.
@@ -109,68 +87,8 @@
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);
- }
}
/**
@@ -190,7 +108,6 @@
* 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,
@@ -201,75 +118,6 @@
}
/**
- * 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,
* or for documents within archives.
@@ -376,127 +224,24 @@
*
* @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.");
+ throws FileNotFoundException;
- final ZipEntry entry = mEntries.get(parsedId.mPath);
- if (entry == null) {
- throw new 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);
}
/**
@@ -508,16 +253,11 @@
@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());
@@ -531,7 +271,7 @@
row.add(Document.COLUMN_FLAGS, flags);
}
- private String getMimeTypeForEntry(ZipEntry entry) {
+ static String getMimeTypeForEntry(ZipEntry entry) {
if (entry.isDirectory()) {
return Document.MIME_TYPE_DIR;
}
@@ -549,7 +289,7 @@
}
// 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)) {
diff --git a/src/com/android/documentsui/archives/ArchiveId.java b/src/com/android/documentsui/archives/ArchiveId.java
index ae23bce..1ce0ede 100644
--- a/src/com/android/documentsui/archives/ArchiveId.java
+++ b/src/com/android/documentsui/archives/ArchiveId.java
@@ -22,22 +22,36 @@
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;
}
};
diff --git a/src/com/android/documentsui/archives/ArchivesProvider.java b/src/com/android/documentsui/archives/ArchivesProvider.java
index 8d5552b..e0ff672 100644
--- a/src/com/android/documentsui/archives/ArchivesProvider.java
+++ b/src/com/android/documentsui/archives/ArchivesProvider.java
@@ -121,7 +121,7 @@
cursor.setExtras(bundle);
cursor.setNotificationUri(getContext().getContentResolver(),
- buildUriForArchive(archiveId.mArchiveUri));
+ buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
return cursor;
} finally {
releaseInstance(loader);
@@ -231,9 +231,15 @@
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());
}
/**
@@ -263,8 +269,7 @@
}
}
- 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);
@@ -281,7 +286,8 @@
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) {
diff --git a/src/com/android/documentsui/archives/Loader.java b/src/com/android/documentsui/archives/Loader.java
index 2e03c39..6ad6011 100644
--- a/src/com/android/documentsui/archives/Loader.java
+++ b/src/com/android/documentsui/archives/Loader.java
@@ -42,6 +42,7 @@
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();
@@ -50,9 +51,10 @@
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.
@@ -77,11 +79,17 @@
}
try {
- mArchive = Archive.createForParcelFileDescriptor(
- mContext,
- mContext.getContentResolver().openFileDescriptor(
- mArchiveUri, "r", null /* signal */),
- mArchiveUri, mNotificationUri);
+ if (ReadableArchive.supportsAccessMode(mAccessMode)) {
+ mArchive = ReadableArchive.createForParcelFileDescriptor(
+ mContext,
+ mContext.getContentResolver().openFileDescriptor(
+ mArchiveUri, "r", null /* signal */),
+ mArchiveUri, mAccessMode, mNotificationUri);
+ // TODO:
+ // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) {
+ } else {
+ throw new IllegalStateException("Access mode not supported.");
+ }
synchronized (mStatusLock) {
mStatus = STATUS_OPENED;
}
@@ -95,7 +103,7 @@
// 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 */);
}
diff --git a/src/com/android/documentsui/archives/ReadableArchive.java b/src/com/android/documentsui/archives/ReadableArchive.java
new file mode 100644
index 0000000..26e8eec
--- /dev/null
+++ b/src/com/android/documentsui/archives/ReadableArchive.java
@@ -0,0 +1,347 @@
+/*
+ * 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.content.res.AssetFileDescriptor;
+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.support.annotation.Nullable;
+import android.util.Log;
+import android.util.jar.StrictJarFile;
+
+import com.android.internal.util.Preconditions;
+
+import libcore.io.IoUtils;
+
+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.Iterator;
+import java.util.List;
+import java.util.Stack;
+import java.util.zip.ZipEntry;
+
+/**
+ * Provides basic implementation for extracting and accessing
+ * files within archives exposed by a document provider.
+ *
+ * <p>This class is thread safe.
+ */
+public class ReadableArchive extends Archive {
+ private static final String TAG = "Archive";
+
+ private final StrictJarFile mZipFile;
+
+ private ReadableArchive(
+ Context context,
+ @Nullable File file,
+ @Nullable 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.");
+ }
+
+ mZipFile = file != null ?
+ new StrictJarFile(file.getPath(), false /* verify */,
+ false /* signatures */) :
+ new StrictJarFile(fd, false /* verify */, false /* signatures */);
+
+ 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);
+ }
+ }
+
+ /**
+ * @see ParcelFileDescriptor
+ */
+ public static boolean supportsAccessMode(int accessMode) {
+ return accessMode == ParcelFileDescriptor.MODE_READ_ONLY;
+ }
+
+ /**
+ * 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 accessMode Access mode for the archive {@see ParcelFileDescriptor}.
+ * @param Uri notificationUri Uri for notifying that the archive file has changed.
+ */
+ public static ReadableArchive createForParcelFileDescriptor(
+ Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode,
+ @Nullable Uri notificationUri)
+ throws IOException {
+ FileDescriptor fd = null;
+ try {
+ if (canSeek(descriptor)) {
+ fd = new FileDescriptor();
+ fd.setInt$(descriptor.detachFd());
+ return new ReadableArchive(context, null, fd, archiveUri, accessMode,
+ 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 ReadableArchive(context, snapshotFile, null, archiveUri, accessMode,
+ 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;
+ }
+ }
+
+ /**
+ * Opens a file within an archive.
+ *
+ * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
+ */
+ 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;
+ 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 InputStream inputStream = mZipFile.getInputStream(entry);
+ final ParcelFileDescriptor outputPipe = pipe[1];
+ 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 = inputStream.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(inputStream);
+ }
+ }
+ });
+
+ return pipe[0];
+ }
+
+ /**
+ * Opens a thumbnail of a file within an archive.
+ *
+ * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
+ */
+ 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();
+ }
+
+ 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);
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ try {
+ mZipFile.close();
+ } catch (IOException e) {
+ // Silent close.
+ }
+ }
+};
diff --git a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
index c196c0c..373ba46 100644
--- a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
+++ b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
@@ -70,7 +70,8 @@
public void testOpen_Success() throws InterruptedException {
final Uri sourceUri = DocumentsContract.buildDocumentUri(
ResourcesProvider.AUTHORITY, "archive.zip");
- final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri);
+ final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
+ ParcelFileDescriptor.MODE_READ_ONLY);
final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
@@ -113,7 +114,8 @@
public void testOpen_Failure() throws InterruptedException {
final Uri sourceUri = DocumentsContract.buildDocumentUri(
ResourcesProvider.AUTHORITY, "broken.zip");
- final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri);
+ final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
+ ParcelFileDescriptor.MODE_READ_ONLY);
final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
diff --git a/tests/unit/com/android/documentsui/archives/ArchiveTest.java b/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
similarity index 82%
rename from tests/unit/com/android/documentsui/archives/ArchiveTest.java
rename to tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
index e4356e5..9f12186 100644
--- a/tests/unit/com/android/documentsui/archives/ArchiveTest.java
+++ b/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
@@ -16,7 +16,7 @@
package com.android.documentsui.archives;
-import com.android.documentsui.archives.Archive;
+import com.android.documentsui.archives.ReadableArchive;
import com.android.documentsui.tests.R;
import android.database.Cursor;
@@ -38,7 +38,7 @@
import java.util.concurrent.TimeUnit;
@MediumTest
-public class ArchiveTest extends AndroidTestCase {
+public class ReadableArchiveTest extends AndroidTestCase {
private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
private static final String NOTIFICATION_URI = "content://notification-uri";
private ExecutorService mExecutor = null;
@@ -64,25 +64,26 @@
}
public static ArchiveId createArchiveId(String path) {
- return new ArchiveId(ARCHIVE_URI, path);
+ return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_READ_ONLY, path);
}
public void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
- mArchive = Archive.createForParcelFileDescriptor(
+ mArchive = ReadableArchive.createForParcelFileDescriptor(
InstrumentationRegistry.getTargetContext(),
descriptor,
ARCHIVE_URI,
+ ParcelFileDescriptor.MODE_READ_ONLY,
Uri.parse(NOTIFICATION_URI));
}
public void testQueryChildDocument() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
final Cursor cursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+ createArchiveId("/").toDocumentId(), null, null);
assertTrue(cursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/file1.txt").toDocumentId(),
+ createArchiveId("/file1.txt").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("file1.txt",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -92,7 +93,7 @@
cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
assertTrue(cursor.moveToNext());
- assertEquals(new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ assertEquals(createArchiveId("/dir1/").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir1",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -103,7 +104,7 @@
assertTrue(cursor.moveToNext());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir2/").toDocumentId(),
+ createArchiveId("/dir2/").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir2",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -116,11 +117,11 @@
// Check if querying children works too.
final Cursor childCursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+ createArchiveId("/dir1/").toDocumentId(), null, null);
assertTrue(childCursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId(),
+ createArchiveId("/dir1/cherries.txt").toDocumentId(),
childCursor.getString(childCursor.getColumnIndexOrThrow(
Document.COLUMN_DOCUMENT_ID)));
assertEquals("cherries.txt",
@@ -136,11 +137,11 @@
public void testQueryChildDocument_NoDirs() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.no_dirs));
final Cursor cursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+ createArchiveId("/").toDocumentId(), null, null);
assertTrue(cursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ createArchiveId("/dir1/").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir1",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -151,11 +152,11 @@
assertFalse(cursor.moveToNext());
final Cursor childCursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+ createArchiveId("/dir1/").toDocumentId(), null, null);
assertTrue(childCursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ createArchiveId("/dir1/dir2/").toDocumentId(),
childCursor.getString(childCursor.getColumnIndexOrThrow(
Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir2",
@@ -169,12 +170,12 @@
assertFalse(childCursor.moveToNext());
final Cursor childCursor2 = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ createArchiveId("/dir1/dir2/").toDocumentId(),
null, null);
assertTrue(childCursor2.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir2/cherries.txt").toDocumentId(),
+ createArchiveId("/dir1/dir2/cherries.txt").toDocumentId(),
childCursor2.getString(childCursor.getColumnIndexOrThrow(
Document.COLUMN_DOCUMENT_ID)));
assertFalse(childCursor2.moveToNext());
@@ -183,11 +184,11 @@
public void testQueryChildDocument_EmptyDirs() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.empty_dirs));
final Cursor cursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+ createArchiveId("/").toDocumentId(), null, null);
assertTrue(cursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ createArchiveId("/dir1/").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir1",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -198,11 +199,11 @@
assertFalse(cursor.moveToNext());
final Cursor childCursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+ createArchiveId("/dir1/").toDocumentId(), null, null);
assertTrue(childCursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ createArchiveId("/dir1/dir2/").toDocumentId(),
childCursor.getString(childCursor.getColumnIndexOrThrow(
Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir2",
@@ -216,7 +217,7 @@
assertTrue(childCursor.moveToNext());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir3/").toDocumentId(),
+ createArchiveId("/dir1/dir3/").toDocumentId(),
childCursor.getString(childCursor.getColumnIndexOrThrow(
Document.COLUMN_DOCUMENT_ID)));
assertEquals("dir3",
@@ -230,12 +231,12 @@
assertFalse(cursor.moveToNext());
final Cursor childCursor2 = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ createArchiveId("/dir1/dir2/").toDocumentId(),
null, null);
assertFalse(childCursor2.moveToFirst());
final Cursor childCursor3 = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/dir1/dir3/").toDocumentId(),
+ createArchiveId("/dir1/dir3/").toDocumentId(),
null, null);
assertFalse(childCursor3.moveToFirst());
}
@@ -243,34 +244,34 @@
public void testGetDocumentType() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
+ createArchiveId("/dir1/").toDocumentId()));
assertEquals("text/plain", mArchive.getDocumentType(
- new ArchiveId(ARCHIVE_URI, "/file1.txt").toDocumentId()));
+ createArchiveId("/file1.txt").toDocumentId()));
}
public void testIsChildDocument() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
- final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId();
+ final String documentId = createArchiveId("/").toDocumentId();
assertTrue(mArchive.isChildDocument(documentId,
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
+ createArchiveId("/dir1/").toDocumentId()));
assertFalse(mArchive.isChildDocument(documentId,
- new ArchiveId(ARCHIVE_URI, "/this-does-not-exist").toDocumentId()));
+ createArchiveId("/this-does-not-exist").toDocumentId()));
assertTrue(mArchive.isChildDocument(
- new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
- new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId()));
+ createArchiveId("/dir1/").toDocumentId(),
+ createArchiveId("/dir1/cherries.txt").toDocumentId()));
assertTrue(mArchive.isChildDocument(documentId,
- new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId()));
+ createArchiveId("/dir1/cherries.txt").toDocumentId()));
}
public void testQueryDocument() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
final Cursor cursor = mArchive.queryDocument(
- new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ createArchiveId("/dir2/strawberries.txt").toDocumentId(),
null);
assertTrue(cursor.moveToFirst());
assertEquals(
- new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ createArchiveId("/dir2/strawberries.txt").toDocumentId(),
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
assertEquals("strawberries.txt",
cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
@@ -293,7 +294,7 @@
// Common part of testOpenDocument and testOpenDocument_NonSeekable.
void commonTestOpenDocument() throws IOException {
final ParcelFileDescriptor descriptor = mArchive.openDocument(
- new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ createArchiveId("/dir2/strawberries.txt").toDocumentId(),
"r", null /* signal */);
try (final ParcelFileDescriptor.AutoCloseInputStream inputStream =
new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) {
@@ -309,6 +310,6 @@
public void testBrokenArchive() throws IOException {
loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
final Cursor cursor = mArchive.queryChildDocuments(
- new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+ createArchiveId("/").toDocumentId(), null, null);
}
}