Remove cache, and manually control archives lifecycle.

This will reduce memory consumption, copying flakyness and
simplify code.

Test: Unit tests.
Bug: 35303895, 35151292
Change-Id: I7124cf3c3ec897887171dffb80eddfe99a6a7c41
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index c0f69fc..8720701 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -30,6 +30,7 @@
 import android.provider.DocumentsContract.Document;
 import android.util.Log;
 
+import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.FilteringCursorWrapper;
 import com.android.documentsui.base.RootInfo;
@@ -92,6 +93,10 @@
         Cursor cursor;
         try {
             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
+            if (mDoc.isInArchive()) {
+                ArchivesProvider.acquireArchive(client, mUri);
+            }
+            result.client = client;
             cursor = client.query(
                     mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal);
             if (cursor == null) {
@@ -108,8 +113,6 @@
             }
 
             cursor = mModel.sortCursor(cursor);
-
-            result.client = client;
             result.cursor = cursor;
         } catch (Exception e) {
             Log.w(TAG, "Failed to query", e);
@@ -118,6 +121,7 @@
             synchronized (this) {
                 mSignal = null;
             }
+            // TODO: Remove this call.
             ContentProviderClient.releaseQuietly(client);
         }
 
diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java
index 74516ca..58746e5 100644
--- a/src/com/android/documentsui/DirectoryResult.java
+++ b/src/com/android/documentsui/DirectoryResult.java
@@ -17,8 +17,10 @@
 package com.android.documentsui;
 
 import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.database.Cursor;
 
+import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.DocumentInfo;
 
 import libcore.io.IoUtils;
@@ -33,7 +35,9 @@
     @Override
     public void close() {
         IoUtils.closeQuietly(cursor);
-        ContentProviderClient.releaseQuietly(client);
+        if (client != null && doc.isInArchive()) {
+            ArchivesProvider.releaseArchive(client, doc.derivedUri);
+        }
         cursor = null;
         client = null;
         doc = null;
diff --git a/src/com/android/documentsui/archives/ArchivesProvider.java b/src/com/android/documentsui/archives/ArchivesProvider.java
index 7eeaad7..cbc8a02 100644
--- a/src/com/android/documentsui/archives/ArchivesProvider.java
+++ b/src/com/android/documentsui/archives/ArchivesProvider.java
@@ -35,7 +35,6 @@
 import android.provider.DocumentsProvider;
 import android.support.annotation.Nullable;
 import android.util.Log;
-import android.util.LruCache;
 
 import com.android.documentsui.R;
 import com.android.internal.annotations.GuardedBy;
@@ -57,37 +56,28 @@
  * <p>This class is thread safe. All methods can be called on any thread without
  * synchronization.
  */
-public class ArchivesProvider extends DocumentsProvider implements Closeable {
+public class ArchivesProvider extends DocumentsProvider {
     public static final String AUTHORITY = "com.android.documentsui.archives";
 
     private static final String TAG = "ArchivesProvider";
-    private static final String METHOD_CLOSE_ARCHIVE = "closeArchive";
-    private static final int OPENED_ARCHIVES_CACHE_SIZE = 4;
+    private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive";
+    private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive";
     private static final String[] ZIP_MIME_TYPES = {
             "application/zip", "application/x-zip", "application/x-zip-compressed"
     };
 
     @GuardedBy("mArchives")
-    private final LruCache<Key, Loader> mArchives =
-            new LruCache<Key, Loader>(OPENED_ARCHIVES_CACHE_SIZE) {
-                @Override
-                public void entryRemoved(boolean evicted, Key key,
-                        Loader oldValue, Loader newValue) {
-                    oldValue.getWriteLock().lock();
-                    try {
-                        oldValue.get().close();
-                    } catch (IOException e) {
-                        Log.e(TAG, "Closing archive failed.", e);
-                    }finally {
-                        oldValue.getWriteLock().unlock();
-                    }
-                }
-            };
+    private final Map<Key, Loader> mArchives = new HashMap<Key, Loader>();
 
     @Override
     public Bundle call(String method, String arg, Bundle extras) {
-        if (METHOD_CLOSE_ARCHIVE.equals(method)) {
-            closeArchive(arg);
+        if (METHOD_ACQUIRE_ARCHIVE.equals(method)) {
+            acquireArchive(arg);
+            return null;
+        }
+
+        if (METHOD_RELEASE_ARCHIVE.equals(method)) {
+            releaseArchive(arg);
             return null;
         }
 
@@ -109,40 +99,35 @@
             @Nullable String sortOrder)
             throws FileNotFoundException {
         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            final int status = loader.getStatus();
-            // If already loaded, then forward the request to the archive.
-            if (status == Loader.STATUS_OPENED) {
-                return loader.get().queryChildDocuments(documentId, projection, sortOrder);
-            }
-
-            final MatrixCursor cursor = new MatrixCursor(
-                    projection != null ? projection : Archive.DEFAULT_PROJECTION);
-            final Bundle bundle = new Bundle();
-
-            switch (status) {
-                case Loader.STATUS_OPENING:
-                    bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
-                    break;
-
-                case Loader.STATUS_FAILED:
-                    // Return an empty cursor with EXTRA_LOADING, which shows spinner
-                    // in DocumentsUI. Once the archive is loaded, the notification will
-                    // be sent, and the directory reloaded.
-                    bundle.putString(DocumentsContract.EXTRA_ERROR,
-                            getContext().getString(R.string.archive_loading_failed));
-                    break;
-            }
-
-            cursor.setExtras(bundle);
-            cursor.setNotificationUri(getContext().getContentResolver(),
-                    buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
-            return cursor;
-        } finally {
-            releaseInstance(loader);
+        final Loader loader = getLoaderOrThrow(documentId);
+        final int status = loader.getStatus();
+        // If already loaded, then forward the request to the archive.
+        if (status == Loader.STATUS_OPENED) {
+            return loader.get().queryChildDocuments(documentId, projection, sortOrder);
         }
+
+        final MatrixCursor cursor = new MatrixCursor(
+                projection != null ? projection : Archive.DEFAULT_PROJECTION);
+        final Bundle bundle = new Bundle();
+
+        switch (status) {
+            case Loader.STATUS_OPENING:
+                bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
+                break;
+
+            case Loader.STATUS_FAILED:
+                // Return an empty cursor with EXTRA_LOADING, which shows spinner
+                // in DocumentsUI. Once the archive is loaded, the notification will
+                // be sent, and the directory reloaded.
+                bundle.putString(DocumentsContract.EXTRA_ERROR,
+                        getContext().getString(R.string.archive_loading_failed));
+                break;
+        }
+
+        cursor.setExtras(bundle);
+        cursor.setNotificationUri(getContext().getContentResolver(),
+                buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
+        return cursor;
     }
 
     @Override
@@ -152,26 +137,14 @@
             return Document.MIME_TYPE_DIR;
         }
 
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            return loader.get().getDocumentType(documentId);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(documentId);
+        return loader.get().getDocumentType(documentId);
     }
 
     @Override
     public boolean isChildDocument(String parentDocumentId, String documentId) {
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            return loader.get().isChildDocument(parentDocumentId, documentId);
-        } catch (FileNotFoundException e) {
-            throw new IllegalStateException(e);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(documentId);
+        return loader.get().isChildDocument(parentDocumentId, documentId);
     }
 
     @Override
@@ -201,52 +174,32 @@
             }
         }
 
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            return loader.get().queryDocument(documentId, projection);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(documentId);
+        return loader.get().queryDocument(documentId, projection);
     }
 
     @Override
     public String createDocument(
             String parentDocumentId, String mimeType, String displayName)
             throws FileNotFoundException {
-        Loader loader = null;
-        try {
-            loader = obtainInstance(parentDocumentId);
-            return loader.get().createDocument(parentDocumentId, mimeType, displayName);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(parentDocumentId);
+        return loader.get().createDocument(parentDocumentId, mimeType, displayName);
     }
 
     @Override
     public ParcelFileDescriptor openDocument(
             String documentId, String mode, final CancellationSignal signal)
             throws FileNotFoundException {
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            return loader.get().openDocument(documentId, mode, signal);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(documentId);
+        return loader.get().openDocument(documentId, mode, signal);
     }
 
     @Override
     public AssetFileDescriptor openDocumentThumbnail(
             String documentId, Point sizeHint, final CancellationSignal signal)
             throws FileNotFoundException {
-        Loader loader = null;
-        try {
-            loader = obtainInstance(documentId);
-            return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
-        } finally {
-            releaseInstance(loader);
-        }
+        final Loader loader = getLoaderOrThrow(documentId);
+        return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
     }
 
     /**
@@ -273,101 +226,80 @@
     }
 
     /**
-     * Closes an archive.
+     * Acquires an archive.
      */
-    public static void closeArchive(ContentResolver resolver, Uri archiveUri) {
+    public static void acquireArchive(ContentProviderClient client, Uri archiveUri) {
         Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
                 "Mismatching authority. Expected: %s, actual: %s.");
         final String documentId = DocumentsContract.getDocumentId(archiveUri);
-        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
 
-        try (final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
-                AUTHORITY)) {
-            client.call(METHOD_CLOSE_ARCHIVE, documentId, null);
+        try {
+            client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null);
         } catch (Exception e) {
-            Log.w(TAG, "Failed to close archive.", e);
+            Log.w(TAG, "Failed to acquire archive.", e);
         }
     }
 
     /**
-     * Closes an archive.
+     * Releases an archive.
      */
-    public void closeArchive(String documentId) {
+    public static void releaseArchive(ContentProviderClient client, Uri archiveUri) {
+        Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
+                "Mismatching authority. Expected: %s, actual: %s.");
+        final String documentId = DocumentsContract.getDocumentId(archiveUri);
+
+        try {
+            client.call(METHOD_RELEASE_ARCHIVE, documentId, null);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to release archive.", e);
+        }
+    }
+
+    /**
+     * The archive won't close until all clients release it.
+     */
+    private void acquireArchive(String documentId) {
         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
         synchronized (mArchives) {
-            mArchives.remove(Key.fromArchiveId(archiveId));
+            final Key key = Key.fromArchiveId(archiveId);
+            Loader loader = mArchives.get(key);
+            if (loader == null) {
+                // TODO: Pass parent Uri so the loader can acquire the parent's notification Uri.
+                loader = new Loader(getContext(), archiveId.mArchiveUri, archiveId.mAccessMode,
+                        null);
+                mArchives.put(key, loader);
+            }
+            loader.acquire();
+            mArchives.put(key, loader);
         }
     }
 
     /**
-     * Closes the helper and disposes all existing archives. It will block until all ongoing
-     * operations on each opened archive are finished.
+     * If all clients release the archive, then it will be closed.
      */
-    @Override
-    // TODO: Wire close() to call().
-    public void close() {
+    private void releaseArchive(String documentId) {
+        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
+        final Key key = Key.fromArchiveId(archiveId);
         synchronized (mArchives) {
-            mArchives.evictAll();
+            final Loader loader = mArchives.get(key);
+            loader.release();
+            final int status = loader.getStatus();
+            if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) {
+                mArchives.remove(key);
+            }
         }
     }
 
-    private Loader obtainInstance(String documentId) throws FileNotFoundException {
-        Loader loader;
-        synchronized (mArchives) {
-            loader = getInstanceUncheckedLocked(documentId);
-            loader.getReadLock().lock();
-        }
-        return loader;
-    }
-
-    private void releaseInstance(@Nullable Loader loader) {
-        if (loader != null) {
-            loader.getReadLock().unlock();
-        }
-    }
-
-    private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException {
+    private Loader getLoaderOrThrow(String documentId) {
         final ArchiveId id = ArchiveId.fromDocumentId(documentId);
         final Key key = Key.fromArchiveId(id);
-        final Loader existingLoader = mArchives.get(key);
-        if (existingLoader != null) {
-            return existingLoader;
+        synchronized (mArchives) {
+            final Loader loader = mArchives.get(key);
+            if (loader == null) {
+                throw new IllegalStateException("Archive not acquired.");
+            }
+            return loader;
         }
-
-        final Cursor cursor = getContext().getContentResolver().query(
-                id.mArchiveUri, new String[] { Document.COLUMN_MIME_TYPE }, null, null, null);
-        if (cursor == null || cursor.getCount() == 0) {
-            throw new FileNotFoundException("File not found." + id.mArchiveUri);
-        }
-
-        cursor.moveToFirst();
-        final String mimeType = cursor.getString(cursor.getColumnIndex(
-                Document.COLUMN_MIME_TYPE));
-        Preconditions.checkArgument(isSupportedArchiveType(mimeType));
-        final Uri notificationUri = cursor.getNotificationUri();
-        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) {
-            final LruCache<Key, Loader> finalArchives = mArchives;
-            getContext().getContentResolver().registerContentObserver(notificationUri,
-                    false,
-                    new ContentObserver(null) {
-                        @Override
-                        public void onChange(boolean selfChange, Uri uri) {
-                            synchronized (mArchives) {
-                                final Loader currentLoader = mArchives.get(key);
-                                if (currentLoader == loader) {
-                                    mArchives.remove(key);
-                                }
-                            }
-                        }
-                    });
-        }
-
-        mArchives.put(key, loader);
-        return loader;
     }
 
     private static class Key {
diff --git a/src/com/android/documentsui/archives/Loader.java b/src/com/android/documentsui/archives/Loader.java
index f65589e..838b42c 100644
--- a/src/com/android/documentsui/archives/Loader.java
+++ b/src/com/android/documentsui/archives/Loader.java
@@ -28,7 +28,6 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
  * Loads an instance of Archive lazily.
@@ -39,16 +38,19 @@
     public static final int STATUS_OPENING = 0;
     public static final int STATUS_OPENED = 1;
     public static final int STATUS_FAILED = 2;
+    public static final int STATUS_CLOSING = 3;
+    public static final int STATUS_CLOSED = 4;
 
     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();
-    private final Object mStatusLock = new Object();
-    @GuardedBy("mStatusLock")
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
     private int mStatus = STATUS_OPENING;
+    @GuardedBy("mLock")
+    private int mRefCount = 0;
     private Archive mArchive = null;
 
     Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) {
@@ -62,19 +64,16 @@
     }
 
     synchronized Archive get() {
-        synchronized (mStatusLock) {
+        synchronized (mLock) {
             if (mStatus == STATUS_OPENED) {
                 return mArchive;
             }
         }
 
-        // Once loading the archive failed, do not to retry opening it until the
-        // archive file has changed (the loader is deleted once we receive
-        // a notification about the archive file being changed).
-        synchronized (mStatusLock) {
-            if (mStatus == STATUS_FAILED) {
+        synchronized (mLock) {
+            if (mStatus != STATUS_OPENING) {
                 throw new IllegalStateException(
-                        "Trying to perform an operation on an archive which failed to load.");
+                        "Trying to perform an operation on an archive which is invalidated.");
             }
         }
 
@@ -94,12 +93,18 @@
             } else {
                 throw new IllegalStateException("Access mode not supported.");
             }
-            synchronized (mStatusLock) {
-                mStatus = STATUS_OPENED;
+            boolean closedDueToRefcount = false;
+            synchronized (mLock) {
+                if (mRefCount == 0) {
+                    mArchive.close();
+                    mStatus = STATUS_CLOSED;
+                } else {
+                    mStatus = STATUS_OPENED;
+                }
             }
         } catch (IOException | RuntimeException e) {
             Log.e(TAG, "Failed to open the archive.", e);
-            synchronized (mStatusLock) {
+            synchronized (mLock) {
                 mStatus = STATUS_FAILED;
             }
             throw new IllegalStateException("Failed to open the archive.", e);
@@ -115,16 +120,34 @@
     }
 
     int getStatus() {
-        synchronized (mStatusLock) {
+        synchronized (mLock) {
             return mStatus;
         }
     }
 
-    Lock getReadLock() {
-        return mLock.readLock();
+    void acquire() {
+        synchronized (mLock) {
+            mRefCount++;
+        }
     }
 
-    Lock getWriteLock() {
-        return mLock.writeLock();
+    void release() {
+        synchronized (mLock) {
+            mRefCount--;
+            if (mRefCount == 0) {
+                if (mStatus == STATUS_OPENED) {
+                    try {
+                        mArchive.close();
+                        mStatus = STATUS_CLOSED;
+                    } catch (IOException e) {
+                        Log.e(TAG, "Failed to close the archive on release.", e);
+                        mStatus = STATUS_FAILED;
+                    }
+                } else {
+                    mStatus = STATUS_CLOSING;
+                    // ::get() will close the archive once opened.
+                }
+            }
+        }
     }
 }
diff --git a/src/com/android/documentsui/services/CompressJob.java b/src/com/android/documentsui/services/CompressJob.java
index d5c49b2..8e43b19 100644
--- a/src/com/android/documentsui/services/CompressJob.java
+++ b/src/com/android/documentsui/services/CompressJob.java
@@ -85,6 +85,10 @@
 
     @Override
     public boolean setUp() {
+        if (!super.setUp()) {
+            return false;
+        }
+
         final ContentResolver resolver = appContext.getContentResolver();
 
         // TODO: Move this to DocumentsProvider.
@@ -95,20 +99,31 @@
         try {
             mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive(
                     archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY));
+            ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri);
         } catch (FileNotFoundException e) {
             Log.e(TAG, "Failed to create dstInfo.", e);
             failureCount = mResourceUris.getItemCount();
             return false;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to acquire the archive.", e);
+            failureCount = mResourceUris.getItemCount();
+            return false;
         }
 
-        return super.setUp();
+        return true;
     }
 
     @Override
     void finish() {
-        final ContentResolver resolver = appContext.getContentResolver();
-        ArchivesProvider.closeArchive(resolver, mDstInfo.derivedUri);
+        try {
+            ArchivesProvider.releaseArchive(getClient(mDstInfo), mDstInfo.derivedUri);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to release the archive.");
+        }
+
         // TODO: Remove the archive file in case of an error.
+
+        super.finish();
     }
 
     /**
diff --git a/src/com/android/documentsui/services/ResolvedResourcesJob.java b/src/com/android/documentsui/services/ResolvedResourcesJob.java
index 145fcee..c4ab71c 100644
--- a/src/com/android/documentsui/services/ResolvedResourcesJob.java
+++ b/src/com/android/documentsui/services/ResolvedResourcesJob.java
@@ -16,11 +16,14 @@
 
 package com.android.documentsui.services;
 
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.net.Uri;
+import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
@@ -68,9 +71,35 @@
             }
         }
 
+        // Acquire all source archives, so they are not gone while copying from.
+        try {
+            for (DocumentInfo doc : mResolvedDocs) {
+                if (doc.isInArchive()) {
+                    ArchivesProvider.acquireArchive(getClient(doc), doc.derivedUri);
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to acquire an archive.");
+            return false;
+        }
+
         return true;
     }
 
+    @Override
+    void finish() {
+        // Release all archives.
+        for (DocumentInfo doc : mResolvedDocs) {
+            if (doc.isInArchive()) {
+                try {
+                    ArchivesProvider.releaseArchive(getClient(doc), doc.derivedUri);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to release an archive.");
+                }
+            }
+        }
+    }
+
     /**
      * Allows sub-classes to exclude files from processing.
      * By default all files are eligible.
diff --git a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
index 373ba46..7bf2aca 100644
--- a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
+++ b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
@@ -20,6 +20,7 @@
 import com.android.documentsui.archives.Archive;
 import com.android.documentsui.tests.R;
 
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -79,6 +80,10 @@
         final ContentResolver resolver = getContext().getContentResolver();
         final CountDownLatch latch = new CountDownLatch(1);
 
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                archiveUri);
+        ArchivesProvider.acquireArchive(client, archiveUri);
+
         {
             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
             assertNotNull("Cursor must not be null. File not found?", cursor);
@@ -109,6 +114,9 @@
             assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
             assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
         }
+
+        ArchivesProvider.releaseArchive(client, archiveUri);
+        client.release();
     }
 
     public void testOpen_Failure() throws InterruptedException {
@@ -123,7 +131,12 @@
         final ContentResolver resolver = getContext().getContentResolver();
         final CountDownLatch latch = new CountDownLatch(1);
 
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                archiveUri);
+        ArchivesProvider.acquireArchive(client, archiveUri);
+
         {
+            // TODO: Close this and any other cursor in this file.
             final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
             assertNotNull("Cursor must not be null. File not found?", cursor);
 
@@ -153,4 +166,51 @@
             assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
             assertFalse(TextUtils.isEmpty(extras.getString(DocumentsContract.EXTRA_ERROR)));
         }
-    }}
+
+        ArchivesProvider.releaseArchive(client, archiveUri);
+        client.release();
+    }
+
+    public void testOpen_ClosesOnRelease() throws InterruptedException {
+        final Uri sourceUri = DocumentsContract.buildDocumentUri(
+                ResourcesProvider.AUTHORITY, "broken.zip");
+        final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri,
+                ParcelFileDescriptor.MODE_READ_ONLY);
+
+        final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
+                ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
+
+        final ContentResolver resolver = getContext().getContentResolver();
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                archiveUri);
+
+        // Acquire twice to ensure that the refcount works correctly.
+        ArchivesProvider.acquireArchive(client, archiveUri);
+        ArchivesProvider.acquireArchive(client, archiveUri);
+
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+        }
+
+        ArchivesProvider.releaseArchive(client, archiveUri);
+
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+        }
+
+        ArchivesProvider.releaseArchive(client, archiveUri);
+
+        try {
+            resolver.query(childrenUri, null, null, null, null, null);
+            fail("The archive was expected to be invalited on the last release call.");
+        } catch (IllegalStateException e) {
+            // Expected.
+        }
+
+        client.release();
+    }
+}