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();
+ }
+}