Fix using StrictJarFile that is @hide API

In order to do the unbundle DocumentsUI, it is not appropriate to
use the @hide API. StrictJarFile is @hide API.

StrictJarFile has the 4 gigabytes limitation and common compress
support the archive size above 4 gigabytes

ReadableArchiveTest has the implicit assumption that the sequence
order of rows has the consistentence for using the other archive
library. To remove the assumption and move to JUnit 4 style.

fix some checkstyle warning.

Fixes: 112696623
Fixes: 110868242
Test: atest DocumentsUITests

Change-Id: Ibaf0b2cc80cbd322c09791f2287734022572f1e4
diff --git a/Android.bp b/Android.bp
index 1458d94..fe6b75a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -9,6 +9,7 @@
         "androidx.recyclerview_recyclerview",
         "androidx.recyclerview_recyclerview-selection",
         "androidx.transition_transition",
+        "apache-commons-compress",
         "com.google.android.material_material",
         "guava",
     ],
diff --git a/build_apk.mk b/build_apk.mk
index 23e19f2..1c9394c 100644
--- a/build_apk.mk
+++ b/build_apk.mk
@@ -1,7 +1,8 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_PRIVILEGED_MODULE := true
 
-LOCAL_STATIC_JAVA_LIBRARIES += guava
+LOCAL_STATIC_JAVA_LIBRARIES += guava \
+        apache-commons-compress
 
 LOCAL_STATIC_ANDROID_LIBRARIES := \
         androidx.legacy_legacy-support-core-ui \
diff --git a/src/com/android/documentsui/archives/Archive.java b/src/com/android/documentsui/archives/Archive.java
index e1119df..5193063 100644
--- a/src/com/android/documentsui/archives/Archive.java
+++ b/src/com/android/documentsui/archives/Archive.java
@@ -24,11 +24,8 @@
 import android.net.Uri;
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
-import android.os.storage.StorageManager;
-import android.provider.DocumentsContract;
-import android.provider.MetadataReader;
 import android.provider.DocumentsContract.Document;
-import androidx.annotation.Nullable;
+import android.provider.MetadataReader;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -36,6 +33,7 @@
 import android.webkit.MimeTypeMap;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
 import androidx.core.util.Preconditions;
 
 import java.io.Closeable;
@@ -45,8 +43,8 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.zip.ZipEntry;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
 
 /**
  * Provides basic implementation for creating, extracting and accessing
@@ -72,11 +70,11 @@
 
     // The container as well as values are guarded by mEntries.
     @GuardedBy("mEntries")
-    final Map<String, ZipEntry> mEntries;
+    final Map<String, ZipArchiveEntry> mEntries;
 
     // The container as well as values and elements of values are guarded by mEntries.
     @GuardedBy("mEntries")
-    final Map<String, List<ZipEntry>> mTree;
+    final Map<String, List<ZipArchiveEntry>> mTree;
 
     Archive(
             Context context,
@@ -95,7 +93,7 @@
     /**
      * Returns a valid, normalized path for an entry.
      */
-    public static String getEntryPath(ZipEntry entry) {
+    public static String getEntryPath(ZipArchiveEntry entry) {
         Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
                 "Ill-formated ZIP-file.");
         if (entry.getName().startsWith("/")) {
@@ -138,11 +136,11 @@
         }
 
         synchronized (mEntries) {
-            final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath);
+            final List<ZipArchiveEntry> parentList = mTree.get(parsedParentId.mPath);
             if (parentList == null) {
                 throw new FileNotFoundException();
             }
-            for (final ZipEntry entry : parentList) {
+            for (final ZipArchiveEntry entry : parentList) {
                 addCursorRow(result, entry);
             }
         }
@@ -160,7 +158,7 @@
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
         synchronized (mEntries) {
-            final ZipEntry entry = mEntries.get(parsedId.mPath);
+            final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
             if (entry == null) {
                 throw new FileNotFoundException();
             }
@@ -181,12 +179,12 @@
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
         synchronized (mEntries) {
-            final ZipEntry entry = mEntries.get(parsedId.mPath);
+            final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
             if (entry == null) {
                 return false;
             }
 
-            final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
+            final ZipArchiveEntry parentEntry = mEntries.get(parsedParentId.mPath);
             if (parentEntry == null || !parentEntry.isDirectory()) {
                 return false;
             }
@@ -213,7 +211,7 @@
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
         synchronized (mEntries) {
-            final ZipEntry entry = mEntries.get(parsedId.mPath);
+            final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
             if (entry == null) {
                 throw new FileNotFoundException();
             }
@@ -270,7 +268,7 @@
     /**
      * Not thread safe.
      */
-    void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
+    void addCursorRow(MatrixCursor cursor, ZipArchiveEntry entry) {
         final MatrixCursor.RowBuilder row = cursor.newRow();
         final ArchiveId parsedId = createArchiveId(getEntryPath(entry));
         row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
@@ -289,7 +287,7 @@
         row.add(Document.COLUMN_FLAGS, flags);
     }
 
-    static String getMimeTypeForEntry(ZipEntry entry) {
+    static String getMimeTypeForEntry(ZipArchiveEntry entry) {
         if (entry.isDirectory()) {
             return Document.MIME_TYPE_DIR;
         }
diff --git a/src/com/android/documentsui/archives/Proxy.java b/src/com/android/documentsui/archives/Proxy.java
index d72d309..fcb29e3 100644
--- a/src/com/android/documentsui/archives/Proxy.java
+++ b/src/com/android/documentsui/archives/Proxy.java
@@ -16,28 +16,27 @@
 
 package com.android.documentsui.archives;
 
+import android.os.FileUtils;
 import android.os.ProxyFileDescriptorCallback;
 import android.system.ErrnoException;
 import android.system.OsConstants;
-import android.util.Log;
-import android.util.jar.StrictJarFile;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.zip.ZipEntry;
 
-import android.os.FileUtils;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
 
 /**
  * Provides a backend for a seekable file descriptors for files in archives.
  */
 public class Proxy extends ProxyFileDescriptorCallback {
-    private final StrictJarFile mFile;
-    private final ZipEntry mEntry;
+    private final ZipFile mFile;
+    private final ZipArchiveEntry mEntry;
     private InputStream mInputStream = null;
     private long mOffset = 0;
 
-    Proxy(StrictJarFile file, ZipEntry entry) throws IOException {
+    Proxy(ZipFile file, ZipArchiveEntry entry) throws IOException {
         mFile = file;
         mEntry = entry;
         recreateInputStream();
@@ -82,7 +81,7 @@
         }
 
         return size - remainingSize;
-   }
+    }
 
     @Override public void onRelease() {
         FileUtils.closeQuietly(mInputStream);
diff --git a/src/com/android/documentsui/archives/ReadableArchive.java b/src/com/android/documentsui/archives/ReadableArchive.java
index e38aaa5..a8a9a92 100644
--- a/src/com/android/documentsui/archives/ReadableArchive.java
+++ b/src/com/android/documentsui/archives/ReadableArchive.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui.archives;
 
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.graphics.Point;
@@ -23,33 +25,31 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.OperationCanceledException;
+import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
 import android.provider.DocumentsContract;
-import androidx.annotation.Nullable;
 import android.util.Log;
-import android.util.jar.StrictJarFile;
 
-import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
 import androidx.core.util.Preconditions;
 
-import android.os.FileUtils;
-
 import java.io.File;
-import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
 import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Enumeration;
 import java.util.List;
-import java.util.Set;
 import java.util.Stack;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.ZipEntry;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.apache.commons.compress.utils.IOUtils;
 
 /**
  * Provides basic implementation for extracting and accessing
@@ -61,12 +61,12 @@
     private static final String TAG = "ReadableArchive";
 
     private final StorageManager mStorageManager;
-    private final StrictJarFile mZipFile;
+    private final ZipFile mZipFile;
+    private final ParcelFileDescriptor mParcelFileDescriptor;
 
     private ReadableArchive(
             Context context,
-            @Nullable File file,
-            @Nullable FileDescriptor fd,
+            @Nullable ParcelFileDescriptor parcelFileDescriptor,
             Uri archiveUri,
             int accessMode,
             @Nullable Uri notificationUri)
@@ -78,17 +78,18 @@
 
         mStorageManager = mContext.getSystemService(StorageManager.class);
 
-        mZipFile = file != null ?
-                new StrictJarFile(file.getPath(), false /* verify */,
-                        false /* signatures */) :
-                new StrictJarFile(fd, false /* verify */, false /* signatures */);
+        if (parcelFileDescriptor == null || parcelFileDescriptor.getFileDescriptor() == null) {
+            throw new IllegalArgumentException("File descriptor is invalid");
+        }
+        mParcelFileDescriptor = parcelFileDescriptor;
+        mZipFile = openArchive(parcelFileDescriptor);
 
-        ZipEntry entry;
+        ZipArchiveEntry entry;
         String entryPath;
-        final Iterator<ZipEntry> it = mZipFile.iterator();
-        final Stack<ZipEntry> stack = new Stack<>();
-        while (it.hasNext()) {
-            entry = it.next();
+        final Enumeration<ZipArchiveEntry> it = mZipFile.getEntries();
+        final Stack<ZipArchiveEntry> stack = new Stack<>();
+        while (it.hasMoreElements()) {
+            entry = it.nextElement();
             if (entry.isDirectory() != entry.getName().endsWith("/")) {
                 throw new IOException(
                         "Directories must have a trailing slash, and files must not.");
@@ -99,7 +100,7 @@
             }
             mEntries.put(entryPath, entry);
             if (entry.isDirectory()) {
-                mTree.put(entryPath, new ArrayList<ZipEntry>());
+                mTree.put(entryPath, new ArrayList<ZipArchiveEntry>());
             }
             if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
                 stack.push(entry);
@@ -108,8 +109,8 @@
 
         int delimiterIndex;
         String parentPath;
-        ZipEntry parentEntry;
-        List<ZipEntry> parentList;
+        ZipArchiveEntry parentEntry;
+        List<ZipArchiveEntry> parentList;
 
         // Go through all directories recursively and build a tree structure.
         while (stack.size() > 0) {
@@ -126,7 +127,7 @@
                 // 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 = new ZipArchiveEntry(parentPath);
                 parentEntry.setSize(0);
                 parentEntry.setTime(entry.getTime());
                 mEntries.put(parentPath, parentEntry);
@@ -144,40 +145,39 @@
     }
 
     /**
+     * To check the access mode is readable.
+     *
      * @see ParcelFileDescriptor
      */
     public static boolean supportsAccessMode(int accessMode) {
-        return accessMode == ParcelFileDescriptor.MODE_READ_ONLY;
+        return accessMode == MODE_READ_ONLY;
     }
 
     /**
      * Creates a DocumentsArchive instance for opening, browsing and accessing
      * documents within the archive passed as a file descriptor.
-     *
+     * <p>
      * If the file descriptor is not seekable, then a snapshot will be created.
-     *
+     * </p><p>
      * This method takes ownership for the passed descriptor. The caller must
      * not use it after passing.
-     *
+     * </p>
      * @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.
+     * @param notificationUri 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);
-            }
+        if (canSeek(descriptor)) {
+            return new ReadableArchive(context, descriptor, archiveUri, accessMode,
+                    notificationUri);
+        }
 
+        try {
             // Fallback for non-seekable file descriptors.
             File snapshotFile = null;
             try {
@@ -202,7 +202,11 @@
                     }
                     outputStream.flush();
                 }
-                return new ReadableArchive(context, snapshotFile, null, archiveUri, accessMode,
+
+                ParcelFileDescriptor snapshotPfd = ParcelFileDescriptor.open(
+                        snapshotFile, MODE_READ_ONLY);
+
+                return new ReadableArchive(context, snapshotPfd, archiveUri, accessMode,
                         notificationUri);
             } finally {
                 // On UNIX the file will be still available for processes which opened it, even
@@ -215,7 +219,6 @@
             // Since the method takes ownership of the passed descriptor, close it
             // on exception.
             FileUtils.closeQuietly(descriptor);
-            FileUtils.closeQuietly(fd);
             throw e;
         }
     }
@@ -230,14 +233,14 @@
         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
-        final ZipEntry entry = mEntries.get(parsedId.mPath);
+        final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
         if (entry == null) {
             throw new FileNotFoundException();
         }
 
         try {
             return mStorageManager.openProxyFileDescriptor(
-                    ParcelFileDescriptor.MODE_READ_ONLY, new Proxy(mZipFile, entry));
+                    MODE_READ_ONLY, new Proxy(mZipFile, entry));
         } catch (IOException e) {
             throw new IllegalStateException(e);
         }
@@ -253,7 +256,7 @@
         Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
                 "Thumbnails only supported for image/* MIME type.");
 
-        final ZipEntry entry = mEntries.get(parsedId.mPath);
+        final ZipArchiveEntry entry = mEntries.get(parsedId.mPath);
         if (entry == null) {
             throw new FileNotFoundException();
         }
@@ -305,6 +308,35 @@
             mZipFile.close();
         } catch (IOException e) {
             // Silent close.
+        } finally {
+            /**
+             * For creating FileInputStream by using FileDescriptor, the file descriptor will not
+             * be closed after FileInputStream closed.
+             */
+            IOUtils.closeQuietly(mParcelFileDescriptor);
         }
     }
-};
+
+    private static ZipFile openArchive(ParcelFileDescriptor parcelFileDescriptor)
+            throws IOException {
+        // TODO: To support multiple archive type
+
+        /**
+         * ZipFile keep the FileChannel instance as member field archive. FileChannel doesn't be
+         * closed until ZipFile.close(). FileChannel.close() invoke
+         * AbstractInterruptibleChannel.close() and then FileChannelImpl.implCloseChannel() is
+         * called. FileChannelImpl.implCloseChannel() will close the member field parent that is
+         * FileInputStream and is assigned in FileInputStream.getChannel().
+         * So, to close ZipFile is to close FileInputStream but not file descriptor.
+         */
+        FileChannel fileChannel = new FileInputStream(parcelFileDescriptor.getFileDescriptor())
+                .getChannel();
+        try {
+            return new ZipFile((SeekableByteChannel)fileChannel);
+        } catch (IOException e) {
+            IOUtils.closeQuietly(fileChannel);
+            IOUtils.closeQuietly(parcelFileDescriptor);
+            throw e;
+        }
+    }
+}
diff --git a/src/com/android/documentsui/archives/WriteableArchive.java b/src/com/android/documentsui/archives/WriteableArchive.java
index 0c3e44e..528e8e5 100644
--- a/src/com/android/documentsui/archives/WriteableArchive.java
+++ b/src/com/android/documentsui/archives/WriteableArchive.java
@@ -19,34 +19,31 @@
 import android.content.Context;
 import android.net.Uri;
 import android.os.CancellationSignal;
+import android.os.FileUtils;
 import android.os.OperationCanceledException;
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.provider.DocumentsContract.Document;
-import androidx.annotation.Nullable;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
-import android.os.FileUtils;
-
-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;
 
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+
 /**
  * Provides basic implementation for creating archives.
  *
@@ -77,17 +74,17 @@
             throw new IllegalStateException("Unsupported access mode.");
         }
 
-        addEntry(null /* no parent */, new ZipEntry("/"));  // Root entry.
+        addEntry(null /* no parent */, new ZipArchiveEntry("/"));  // Root entry.
         mOutputStream = new AutoCloseOutputStream(fd);
         mZipOutputStream = new ZipOutputStream(mOutputStream);
     }
 
-    private void addEntry(@Nullable ZipEntry parentEntry, ZipEntry entry) {
+    private void addEntry(@Nullable ZipArchiveEntry parentEntry, ZipArchiveEntry entry) {
         final String entryPath = getEntryPath(entry);
         synchronized (mEntries) {
             if (entry.isDirectory()) {
                 if (!mTree.containsKey(entryPath)) {
-                    mTree.put(entryPath, new ArrayList<ZipEntry>());
+                    mTree.put(entryPath, new ArrayList<ZipArchiveEntry>());
                 }
             }
             mEntries.put(entryPath, entry);
@@ -115,7 +112,7 @@
      * @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.
+     * @param notificationUri notificationUri Uri for notifying that the archive file has changed.
      */
     @VisibleForTesting
     public static WriteableArchive createForParcelFileDescriptor(
@@ -142,17 +139,18 @@
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
         final boolean isDirectory = Document.MIME_TYPE_DIR.equals(mimeType);
-        ZipEntry entry;
+        ZipArchiveEntry entry;
         String entryPath;
 
         synchronized (mEntries) {
-            final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
+            final ZipArchiveEntry parentEntry = mEntries.get(parsedParentId.mPath);
 
             if (parentEntry == null) {
                 throw new FileNotFoundException();
             }
 
-            if (displayName.indexOf("/") != -1 || ".".equals(displayName) || "..".equals(displayName)) {
+            if (displayName.indexOf("/") != -1 || ".".equals(displayName)
+                    || "..".equals(displayName)) {
                 throw new IllegalStateException("Display name contains invalid characters.");
             }
 
@@ -162,9 +160,10 @@
 
 
             assert(parentEntry.getName().endsWith("/"));
-            final String parentName = "/".equals(parentEntry.getName()) ? "" : parentEntry.getName();
+            final String parentName = "/".equals(parentEntry.getName())
+                    ? "" : parentEntry.getName();
             final String entryName = parentName + displayName + (isDirectory ? "/" : "");
-            entry = new ZipEntry(entryName);
+            entry = new ZipArchiveEntry(entryName);
             entryPath = getEntryPath(entry);
             entry.setSize(0);
 
@@ -206,7 +205,7 @@
         MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
                 "Mismatching archive Uri. Expected: %s, actual: %s.");
 
-        final ZipEntry entry;
+        final ZipArchiveEntry entry;
         synchronized (mEntries) {
             entry = mEntries.get(parsedId.mPath);
             if (entry == null) {
@@ -313,4 +312,4 @@
 
         FileUtils.closeQuietly(mOutputStream);
     }
-};
+}
diff --git a/tests/functional/com/android/documentsui/FileCopyUiTest.java b/tests/functional/com/android/documentsui/FileCopyUiTest.java
index c0cd84f..1203483 100644
--- a/tests/functional/com/android/documentsui/FileCopyUiTest.java
+++ b/tests/functional/com/android/documentsui/FileCopyUiTest.java
@@ -41,13 +41,15 @@
 import com.android.documentsui.files.FilesActivity;
 import com.android.documentsui.services.TestNotificationService;
 
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
 
 /**
 * This class test the below points
@@ -247,20 +249,19 @@
 
     private void loadImageFromResources(Uri root, DocumentsProviderHelper helper, int resId,
             Resources res) throws Exception {
-        ZipInputStream in = null;
+        ZipArchiveInputStream in = null;
         int read = 0;
         int count = 0;
         try {
-            in = new ZipInputStream(res.openRawResource(resId));
-            ZipEntry zipEntry = null;
-            while ((zipEntry = in.getNextEntry()) != null && (count++ < TARGET_COUNT)) {
-                String fileName = zipEntry.getName();
+            in = new ZipArchiveInputStream(res.openRawResource(resId));
+            ArchiveEntry archiveEntry = null;
+            while ((archiveEntry = in.getNextEntry()) != null && (count++ < TARGET_COUNT)) {
+                String fileName = archiveEntry.getName();
                 Uri uri = helper.createDocument(root, "image/png", fileName);
                 byte[] buff = new byte[1024];
                 while ((read = in.read(buff)) > 0) {
                     helper.writeAppendDocument(uri, buff);
                 }
-                in.closeEntry();
                 buff = null;
             }
         } finally {
diff --git a/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java b/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
index 98e4f09..49878db 100644
--- a/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
+++ b/tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java
@@ -16,32 +16,38 @@
 
 package com.android.documentsui.archives;
 
-import com.android.documentsui.archives.ReadableArchive;
-import com.android.documentsui.tests.R;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
-import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract.Document;
 import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
 
-import java.io.File;
-import java.io.FileOutputStream;
+import com.android.documentsui.tests.R;
+
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.Scanner;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
 @MediumTest
-public class ReadableArchiveTest extends AndroidTestCase {
+public class ReadableArchiveTest {
     private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
     private static final String NOTIFICATION_URI =
             "content://com.android.documentsui.archives/notification-uri";
@@ -49,29 +55,27 @@
     private Archive mArchive = null;
     private TestUtils mTestUtils = null;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
-        super.setUp();
         mExecutor = Executors.newSingleThreadExecutor();
         mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
                 InstrumentationRegistry.getContext(), mExecutor);
     }
 
-    @Override
+    @After
     public void tearDown() throws Exception {
         mExecutor.shutdown();
         assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
         if (mArchive != null) {
             mArchive.close();
         }
-        super.tearDown();
     }
 
     public static ArchiveId createArchiveId(String path) {
         return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_READ_ONLY, path);
     }
 
-    public void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
+    private void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
         mArchive = ReadableArchive.createForParcelFileDescriptor(
                 InstrumentationRegistry.getTargetContext(),
                 descriptor,
@@ -80,15 +84,30 @@
                 Uri.parse(NOTIFICATION_URI));
     }
 
+    private static void assertRowExist(Cursor cursor, String targetDocId) {
+        assertTrue(cursor.moveToFirst());
+
+        boolean found = false;
+        final int count = cursor.getCount();
+        for (int i = 0; i < count; i++) {
+            cursor.moveToPosition(i);
+            if (TextUtils.equals(targetDocId, cursor.getString(
+                    cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))) {
+                found = true;
+                break;
+            }
+        }
+
+        assertTrue(targetDocId + " should be exists", found);
+    }
+
+    @Test
     public void testQueryChildDocument() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final Cursor cursor = mArchive.queryChildDocuments(
                 createArchiveId("/").toDocumentId(), null, null);
 
-        assertTrue(cursor.moveToFirst());
-        assertEquals(
-                createArchiveId("/file1.txt").toDocumentId(),
-                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+        assertRowExist(cursor, createArchiveId("/file1.txt").toDocumentId());
         assertEquals("file1.txt",
                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
         assertEquals("text/plain",
@@ -96,9 +115,7 @@
         assertEquals(13,
                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
 
-        assertTrue(cursor.moveToNext());
-        assertEquals(createArchiveId("/dir1/").toDocumentId(),
-                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+        assertRowExist(cursor, createArchiveId("/dir1/").toDocumentId());
         assertEquals("dir1",
                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
         assertEquals(Document.MIME_TYPE_DIR,
@@ -106,10 +123,7 @@
         assertEquals(0,
                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
 
-        assertTrue(cursor.moveToNext());
-        assertEquals(
-                createArchiveId("/dir2/").toDocumentId(),
-                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+        assertRowExist(cursor, createArchiveId("/dir2/").toDocumentId());
         assertEquals("dir2",
                 cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
         assertEquals(Document.MIME_TYPE_DIR,
@@ -117,7 +131,6 @@
         assertEquals(0,
                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
 
-        assertFalse(cursor.moveToNext());
 
         // Check if querying children works too.
         final Cursor childCursor = mArchive.queryChildDocuments(
@@ -138,6 +151,7 @@
                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
     }
 
+    @Test
     public void testQueryChildDocument_NoDirs() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.no_dirs));
         final Cursor cursor = mArchive.queryChildDocuments(
@@ -185,6 +199,7 @@
         assertFalse(childCursor2.moveToNext());
     }
 
+    @Test
     public void testQueryChildDocument_EmptyDirs() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.empty_dirs));
         final Cursor cursor = mArchive.queryChildDocuments(
@@ -205,11 +220,7 @@
         final Cursor childCursor = mArchive.queryChildDocuments(
                 createArchiveId("/dir1/").toDocumentId(), null, null);
 
-        assertTrue(childCursor.moveToFirst());
-        assertEquals(
-                createArchiveId("/dir1/dir2/").toDocumentId(),
-                childCursor.getString(childCursor.getColumnIndexOrThrow(
-                        Document.COLUMN_DOCUMENT_ID)));
+        assertRowExist(childCursor, createArchiveId("/dir1/dir2/").toDocumentId());
         assertEquals("dir2",
                 childCursor.getString(childCursor.getColumnIndexOrThrow(
                         Document.COLUMN_DISPLAY_NAME)));
@@ -219,7 +230,7 @@
         assertEquals(0,
                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
 
-        assertTrue(childCursor.moveToNext());
+        assertRowExist(childCursor, createArchiveId("/dir1/dir3/").toDocumentId());
         assertEquals(
                 createArchiveId("/dir1/dir3/").toDocumentId(),
                 childCursor.getString(childCursor.getColumnIndexOrThrow(
@@ -232,7 +243,6 @@
                         Document.COLUMN_MIME_TYPE)));
         assertEquals(0,
                 childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
-        assertFalse(cursor.moveToNext());
 
         final Cursor childCursor2 = mArchive.queryChildDocuments(
                 createArchiveId("/dir1/dir2/").toDocumentId(),
@@ -245,6 +255,7 @@
         assertFalse(childCursor3.moveToFirst());
     }
 
+    @Test
     public void testGetDocumentType() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
@@ -253,6 +264,7 @@
                 createArchiveId("/file1.txt").toDocumentId()));
     }
 
+    @Test
     public void testIsChildDocument() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final String documentId = createArchiveId("/").toDocumentId();
@@ -267,6 +279,7 @@
                 createArchiveId("/dir1/cherries.txt").toDocumentId()));
     }
 
+    @Test
     public void testQueryDocument() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final Cursor cursor = mArchive.queryDocument(
@@ -285,11 +298,13 @@
                 cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
     }
 
+    @Test
     public void testOpenDocument() throws IOException, ErrnoException {
         loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive));
         commonTestOpenDocument();
     }
 
+    @Test
     public void testOpenDocument_NonSeekable() throws IOException, ErrnoException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         commonTestOpenDocument();
@@ -310,11 +325,13 @@
         }
     }
 
+    @Test
     public void testCanSeek() throws IOException {
         assertTrue(Archive.canSeek(mTestUtils.getSeekableDescriptor(R.raw.archive)));
         assertFalse(Archive.canSeek(mTestUtils.getNonSeekableDescriptor(R.raw.archive)));
     }
 
+    @Test
     public void testBrokenArchive() throws IOException {
         loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final Cursor cursor = mArchive.queryChildDocuments(