Loading src/com/android/documentsui/DirectoryLoader.java +6 −2 Original line number Original line Diff line number Diff line Loading @@ -30,6 +30,7 @@ import android.os.RemoteException; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Document; import android.util.Log; import android.util.Log; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.RootInfo; Loading Loading @@ -92,6 +93,10 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { Cursor cursor; Cursor cursor; try { try { client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); if (mDoc.isInArchive()) { ArchivesProvider.acquireArchive(client, mUri); } result.client = client; cursor = client.query( cursor = client.query( mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal); mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal); if (cursor == null) { if (cursor == null) { Loading @@ -108,8 +113,6 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { } } cursor = mModel.sortCursor(cursor); cursor = mModel.sortCursor(cursor); result.client = client; result.cursor = cursor; result.cursor = cursor; } catch (Exception e) { } catch (Exception e) { Log.w(TAG, "Failed to query", e); Log.w(TAG, "Failed to query", e); Loading @@ -118,6 +121,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { synchronized (this) { synchronized (this) { mSignal = null; mSignal = null; } } // TODO: Remove this call. ContentProviderClient.releaseQuietly(client); ContentProviderClient.releaseQuietly(client); } } Loading src/com/android/documentsui/DirectoryResult.java +5 −1 Original line number Original line Diff line number Diff line Loading @@ -17,8 +17,10 @@ package com.android.documentsui; package com.android.documentsui; import android.content.ContentProviderClient; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.database.Cursor; import android.database.Cursor; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentInfo; import libcore.io.IoUtils; import libcore.io.IoUtils; Loading @@ -33,7 +35,9 @@ public class DirectoryResult implements AutoCloseable { @Override @Override public void close() { public void close() { IoUtils.closeQuietly(cursor); IoUtils.closeQuietly(cursor); ContentProviderClient.releaseQuietly(client); if (client != null && doc.isInArchive()) { ArchivesProvider.releaseArchive(client, doc.derivedUri); } cursor = null; cursor = null; client = null; client = null; doc = null; doc = null; Loading src/com/android/documentsui/archives/ArchivesProvider.java +97 −165 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.provider.DocumentsProvider; import android.support.annotation.Nullable; import android.support.annotation.Nullable; import android.util.Log; import android.util.Log; import android.util.LruCache; import com.android.documentsui.R; import com.android.documentsui.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy; Loading @@ -57,37 +56,28 @@ import java.util.concurrent.locks.Lock; * <p>This class is thread safe. All methods can be called on any thread without * <p>This class is thread safe. All methods can be called on any thread without * synchronization. * synchronization. */ */ public class ArchivesProvider extends DocumentsProvider implements Closeable { public class ArchivesProvider extends DocumentsProvider { public static final String AUTHORITY = "com.android.documentsui.archives"; public static final String AUTHORITY = "com.android.documentsui.archives"; private static final String TAG = "ArchivesProvider"; private static final String TAG = "ArchivesProvider"; private static final String METHOD_CLOSE_ARCHIVE = "closeArchive"; private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive"; private static final int OPENED_ARCHIVES_CACHE_SIZE = 4; private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive"; private static final String[] ZIP_MIME_TYPES = { private static final String[] ZIP_MIME_TYPES = { "application/zip", "application/x-zip", "application/x-zip-compressed" "application/zip", "application/x-zip", "application/x-zip-compressed" }; }; @GuardedBy("mArchives") @GuardedBy("mArchives") private final LruCache<Key, Loader> mArchives = private final Map<Key, Loader> mArchives = new HashMap<Key, Loader>(); 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(); } } }; @Override @Override public Bundle call(String method, String arg, Bundle extras) { public Bundle call(String method, String arg, Bundle extras) { if (METHOD_CLOSE_ARCHIVE.equals(method)) { if (METHOD_ACQUIRE_ARCHIVE.equals(method)) { closeArchive(arg); acquireArchive(arg); return null; } if (METHOD_RELEASE_ARCHIVE.equals(method)) { releaseArchive(arg); return null; return null; } } Loading @@ -109,9 +99,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { @Nullable String sortOrder) @Nullable String sortOrder) throws FileNotFoundException { throws FileNotFoundException { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); final int status = loader.getStatus(); final int status = loader.getStatus(); // If already loaded, then forward the request to the archive. // If already loaded, then forward the request to the archive. if (status == Loader.STATUS_OPENED) { if (status == Loader.STATUS_OPENED) { Loading Loading @@ -140,9 +128,6 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { cursor.setNotificationUri(getContext().getContentResolver(), cursor.setNotificationUri(getContext().getContentResolver(), buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); return cursor; return cursor; } finally { releaseInstance(loader); } } } @Override @Override Loading @@ -152,26 +137,14 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { return Document.MIME_TYPE_DIR; return Document.MIME_TYPE_DIR; } } Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().getDocumentType(documentId); return loader.get().getDocumentType(documentId); } finally { releaseInstance(loader); } } } @Override @Override public boolean isChildDocument(String parentDocumentId, String documentId) { public boolean isChildDocument(String parentDocumentId, String documentId) { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().isChildDocument(parentDocumentId, documentId); return loader.get().isChildDocument(parentDocumentId, documentId); } catch (FileNotFoundException e) { throw new IllegalStateException(e); } finally { releaseInstance(loader); } } } @Override @Override Loading Loading @@ -201,52 +174,32 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } } } Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().queryDocument(documentId, projection); return loader.get().queryDocument(documentId, projection); } finally { releaseInstance(loader); } } } @Override @Override public String createDocument( public String createDocument( String parentDocumentId, String mimeType, String displayName) String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(parentDocumentId); try { loader = obtainInstance(parentDocumentId); return loader.get().createDocument(parentDocumentId, mimeType, displayName); return loader.get().createDocument(parentDocumentId, mimeType, displayName); } finally { releaseInstance(loader); } } } @Override @Override public ParcelFileDescriptor openDocument( public ParcelFileDescriptor openDocument( String documentId, String mode, final CancellationSignal signal) String documentId, String mode, final CancellationSignal signal) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().openDocument(documentId, mode, signal); return loader.get().openDocument(documentId, mode, signal); } finally { releaseInstance(loader); } } } @Override @Override public AssetFileDescriptor openDocumentThumbnail( public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); } finally { releaseInstance(loader); } } } /** /** Loading @@ -273,102 +226,81 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } /** /** * 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(), Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), "Mismatching authority. Expected: %s, actual: %s."); "Mismatching authority. Expected: %s, actual: %s."); final String documentId = DocumentsContract.getDocumentId(archiveUri); final String documentId = DocumentsContract.getDocumentId(archiveUri); final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); try (final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( try { AUTHORITY)) { client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null); client.call(METHOD_CLOSE_ARCHIVE, documentId, null); } catch (Exception e) { } 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) { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), synchronized (mArchives) { "Mismatching authority. Expected: %s, actual: %s."); mArchives.remove(Key.fromArchiveId(archiveId)); 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); } } } } /** /** * Closes the helper and disposes all existing archives. It will block until all ongoing * The archive won't close until all clients release it. * operations on each opened archive are finished. */ */ @Override private void acquireArchive(String documentId) { // TODO: Wire close() to call(). final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); public void close() { synchronized (mArchives) { synchronized (mArchives) { mArchives.evictAll(); 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); } } } } private Loader obtainInstance(String documentId) throws FileNotFoundException { /** Loader loader; * If all clients release the archive, then it will be closed. */ private void releaseArchive(String documentId) { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); final Key key = Key.fromArchiveId(archiveId); synchronized (mArchives) { synchronized (mArchives) { loader = getInstanceUncheckedLocked(documentId); final Loader loader = mArchives.get(key); loader.getReadLock().lock(); loader.release(); final int status = loader.getStatus(); if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) { mArchives.remove(key); } } 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 ArchiveId id = ArchiveId.fromDocumentId(documentId); final Key key = Key.fromArchiveId(id); final Key key = Key.fromArchiveId(id); final Loader existingLoader = mArchives.get(key); if (existingLoader != null) { return existingLoader; } 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) { synchronized (mArchives) { final Loader currentLoader = mArchives.get(key); final Loader loader = mArchives.get(key); if (currentLoader == loader) { if (loader == null) { mArchives.remove(key); throw new IllegalStateException("Archive not acquired."); } } } } }); } mArchives.put(key, loader); return loader; return loader; } } } private static class Key { private static class Key { Uri archiveUri; Uri archiveUri; Loading src/com/android/documentsui/archives/Loader.java +42 −19 Original line number Original line Diff line number Diff line Loading @@ -28,7 +28,6 @@ import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** /** * Loads an instance of Archive lazily. * Loads an instance of Archive lazily. Loading @@ -39,16 +38,19 @@ public class Loader { public static final int STATUS_OPENING = 0; public static final int STATUS_OPENING = 0; public static final int STATUS_OPENED = 1; public static final int STATUS_OPENED = 1; public static final int STATUS_FAILED = 2; 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 Context mContext; private final Uri mArchiveUri; private final Uri mArchiveUri; private final int mAccessMode; private final int mAccessMode; private final Uri mNotificationUri; private final Uri mNotificationUri; private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final Object mStatusLock = new Object(); private final Object mLock = new Object(); @GuardedBy("mStatusLock") @GuardedBy("mLock") private int mStatus = STATUS_OPENING; private int mStatus = STATUS_OPENING; @GuardedBy("mLock") private int mRefCount = 0; private Archive mArchive = null; private Archive mArchive = null; Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { Loading @@ -62,19 +64,16 @@ public class Loader { } } synchronized Archive get() { synchronized Archive get() { synchronized (mStatusLock) { synchronized (mLock) { if (mStatus == STATUS_OPENED) { if (mStatus == STATUS_OPENED) { return mArchive; return mArchive; } } } } // Once loading the archive failed, do not to retry opening it until the synchronized (mLock) { // archive file has changed (the loader is deleted once we receive if (mStatus != STATUS_OPENING) { // a notification about the archive file being changed). synchronized (mStatusLock) { if (mStatus == STATUS_FAILED) { throw new IllegalStateException( 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."); } } } } Loading @@ -94,12 +93,18 @@ public class Loader { } else { } else { throw new IllegalStateException("Access mode not supported."); throw new IllegalStateException("Access mode not supported."); } } synchronized (mStatusLock) { boolean closedDueToRefcount = false; synchronized (mLock) { if (mRefCount == 0) { mArchive.close(); mStatus = STATUS_CLOSED; } else { mStatus = STATUS_OPENED; mStatus = STATUS_OPENED; } } } } catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) { Log.e(TAG, "Failed to open the archive.", e); Log.e(TAG, "Failed to open the archive.", e); synchronized (mStatusLock) { synchronized (mLock) { mStatus = STATUS_FAILED; mStatus = STATUS_FAILED; } } throw new IllegalStateException("Failed to open the archive.", e); throw new IllegalStateException("Failed to open the archive.", e); Loading @@ -115,16 +120,34 @@ public class Loader { } } int getStatus() { int getStatus() { synchronized (mStatusLock) { synchronized (mLock) { return mStatus; return mStatus; } } } } Lock getReadLock() { void acquire() { return mLock.readLock(); synchronized (mLock) { mRefCount++; } } } Lock getWriteLock() { void release() { return mLock.writeLock(); 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. } } } } } } } src/com/android/documentsui/services/CompressJob.java +18 −3 Original line number Original line Diff line number Diff line Loading @@ -85,6 +85,10 @@ final class CompressJob extends CopyJob { @Override @Override public boolean setUp() { public boolean setUp() { if (!super.setUp()) { return false; } final ContentResolver resolver = appContext.getContentResolver(); final ContentResolver resolver = appContext.getContentResolver(); // TODO: Move this to DocumentsProvider. // TODO: Move this to DocumentsProvider. Loading @@ -95,20 +99,31 @@ final class CompressJob extends CopyJob { try { try { mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive( mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive( archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY)); archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY)); ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri); } catch (FileNotFoundException e) { } catch (FileNotFoundException e) { Log.e(TAG, "Failed to create dstInfo.", e); Log.e(TAG, "Failed to create dstInfo.", e); failureCount = mResourceUris.getItemCount(); failureCount = mResourceUris.getItemCount(); return false; 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 @Override void finish() { void finish() { final ContentResolver resolver = appContext.getContentResolver(); try { ArchivesProvider.closeArchive(resolver, mDstInfo.derivedUri); 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. // TODO: Remove the archive file in case of an error. super.finish(); } } /** /** Loading Loading
src/com/android/documentsui/DirectoryLoader.java +6 −2 Original line number Original line Diff line number Diff line Loading @@ -30,6 +30,7 @@ import android.os.RemoteException; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Document; import android.util.Log; import android.util.Log; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.RootInfo; Loading Loading @@ -92,6 +93,10 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { Cursor cursor; Cursor cursor; try { try { client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); if (mDoc.isInArchive()) { ArchivesProvider.acquireArchive(client, mUri); } result.client = client; cursor = client.query( cursor = client.query( mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal); mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal); if (cursor == null) { if (cursor == null) { Loading @@ -108,8 +113,6 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { } } cursor = mModel.sortCursor(cursor); cursor = mModel.sortCursor(cursor); result.client = client; result.cursor = cursor; result.cursor = cursor; } catch (Exception e) { } catch (Exception e) { Log.w(TAG, "Failed to query", e); Log.w(TAG, "Failed to query", e); Loading @@ -118,6 +121,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { synchronized (this) { synchronized (this) { mSignal = null; mSignal = null; } } // TODO: Remove this call. ContentProviderClient.releaseQuietly(client); ContentProviderClient.releaseQuietly(client); } } Loading
src/com/android/documentsui/DirectoryResult.java +5 −1 Original line number Original line Diff line number Diff line Loading @@ -17,8 +17,10 @@ package com.android.documentsui; package com.android.documentsui; import android.content.ContentProviderClient; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.database.Cursor; import android.database.Cursor; import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentInfo; import libcore.io.IoUtils; import libcore.io.IoUtils; Loading @@ -33,7 +35,9 @@ public class DirectoryResult implements AutoCloseable { @Override @Override public void close() { public void close() { IoUtils.closeQuietly(cursor); IoUtils.closeQuietly(cursor); ContentProviderClient.releaseQuietly(client); if (client != null && doc.isInArchive()) { ArchivesProvider.releaseArchive(client, doc.derivedUri); } cursor = null; cursor = null; client = null; client = null; doc = null; doc = null; Loading
src/com/android/documentsui/archives/ArchivesProvider.java +97 −165 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.provider.DocumentsProvider; import android.support.annotation.Nullable; import android.support.annotation.Nullable; import android.util.Log; import android.util.Log; import android.util.LruCache; import com.android.documentsui.R; import com.android.documentsui.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy; Loading @@ -57,37 +56,28 @@ import java.util.concurrent.locks.Lock; * <p>This class is thread safe. All methods can be called on any thread without * <p>This class is thread safe. All methods can be called on any thread without * synchronization. * synchronization. */ */ public class ArchivesProvider extends DocumentsProvider implements Closeable { public class ArchivesProvider extends DocumentsProvider { public static final String AUTHORITY = "com.android.documentsui.archives"; public static final String AUTHORITY = "com.android.documentsui.archives"; private static final String TAG = "ArchivesProvider"; private static final String TAG = "ArchivesProvider"; private static final String METHOD_CLOSE_ARCHIVE = "closeArchive"; private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive"; private static final int OPENED_ARCHIVES_CACHE_SIZE = 4; private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive"; private static final String[] ZIP_MIME_TYPES = { private static final String[] ZIP_MIME_TYPES = { "application/zip", "application/x-zip", "application/x-zip-compressed" "application/zip", "application/x-zip", "application/x-zip-compressed" }; }; @GuardedBy("mArchives") @GuardedBy("mArchives") private final LruCache<Key, Loader> mArchives = private final Map<Key, Loader> mArchives = new HashMap<Key, Loader>(); 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(); } } }; @Override @Override public Bundle call(String method, String arg, Bundle extras) { public Bundle call(String method, String arg, Bundle extras) { if (METHOD_CLOSE_ARCHIVE.equals(method)) { if (METHOD_ACQUIRE_ARCHIVE.equals(method)) { closeArchive(arg); acquireArchive(arg); return null; } if (METHOD_RELEASE_ARCHIVE.equals(method)) { releaseArchive(arg); return null; return null; } } Loading @@ -109,9 +99,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { @Nullable String sortOrder) @Nullable String sortOrder) throws FileNotFoundException { throws FileNotFoundException { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); final int status = loader.getStatus(); final int status = loader.getStatus(); // If already loaded, then forward the request to the archive. // If already loaded, then forward the request to the archive. if (status == Loader.STATUS_OPENED) { if (status == Loader.STATUS_OPENED) { Loading Loading @@ -140,9 +128,6 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { cursor.setNotificationUri(getContext().getContentResolver(), cursor.setNotificationUri(getContext().getContentResolver(), buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); return cursor; return cursor; } finally { releaseInstance(loader); } } } @Override @Override Loading @@ -152,26 +137,14 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { return Document.MIME_TYPE_DIR; return Document.MIME_TYPE_DIR; } } Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().getDocumentType(documentId); return loader.get().getDocumentType(documentId); } finally { releaseInstance(loader); } } } @Override @Override public boolean isChildDocument(String parentDocumentId, String documentId) { public boolean isChildDocument(String parentDocumentId, String documentId) { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().isChildDocument(parentDocumentId, documentId); return loader.get().isChildDocument(parentDocumentId, documentId); } catch (FileNotFoundException e) { throw new IllegalStateException(e); } finally { releaseInstance(loader); } } } @Override @Override Loading Loading @@ -201,52 +174,32 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } } } Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().queryDocument(documentId, projection); return loader.get().queryDocument(documentId, projection); } finally { releaseInstance(loader); } } } @Override @Override public String createDocument( public String createDocument( String parentDocumentId, String mimeType, String displayName) String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(parentDocumentId); try { loader = obtainInstance(parentDocumentId); return loader.get().createDocument(parentDocumentId, mimeType, displayName); return loader.get().createDocument(parentDocumentId, mimeType, displayName); } finally { releaseInstance(loader); } } } @Override @Override public ParcelFileDescriptor openDocument( public ParcelFileDescriptor openDocument( String documentId, String mode, final CancellationSignal signal) String documentId, String mode, final CancellationSignal signal) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().openDocument(documentId, mode, signal); return loader.get().openDocument(documentId, mode, signal); } finally { releaseInstance(loader); } } } @Override @Override public AssetFileDescriptor openDocumentThumbnail( public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal) String documentId, Point sizeHint, final CancellationSignal signal) throws FileNotFoundException { throws FileNotFoundException { Loader loader = null; final Loader loader = getLoaderOrThrow(documentId); try { loader = obtainInstance(documentId); return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); } finally { releaseInstance(loader); } } } /** /** Loading @@ -273,102 +226,81 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable { } } /** /** * 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(), Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), "Mismatching authority. Expected: %s, actual: %s."); "Mismatching authority. Expected: %s, actual: %s."); final String documentId = DocumentsContract.getDocumentId(archiveUri); final String documentId = DocumentsContract.getDocumentId(archiveUri); final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); try (final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( try { AUTHORITY)) { client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null); client.call(METHOD_CLOSE_ARCHIVE, documentId, null); } catch (Exception e) { } 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) { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), synchronized (mArchives) { "Mismatching authority. Expected: %s, actual: %s."); mArchives.remove(Key.fromArchiveId(archiveId)); 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); } } } } /** /** * Closes the helper and disposes all existing archives. It will block until all ongoing * The archive won't close until all clients release it. * operations on each opened archive are finished. */ */ @Override private void acquireArchive(String documentId) { // TODO: Wire close() to call(). final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); public void close() { synchronized (mArchives) { synchronized (mArchives) { mArchives.evictAll(); 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); } } } } private Loader obtainInstance(String documentId) throws FileNotFoundException { /** Loader loader; * If all clients release the archive, then it will be closed. */ private void releaseArchive(String documentId) { final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); final Key key = Key.fromArchiveId(archiveId); synchronized (mArchives) { synchronized (mArchives) { loader = getInstanceUncheckedLocked(documentId); final Loader loader = mArchives.get(key); loader.getReadLock().lock(); loader.release(); final int status = loader.getStatus(); if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) { mArchives.remove(key); } } 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 ArchiveId id = ArchiveId.fromDocumentId(documentId); final Key key = Key.fromArchiveId(id); final Key key = Key.fromArchiveId(id); final Loader existingLoader = mArchives.get(key); if (existingLoader != null) { return existingLoader; } 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) { synchronized (mArchives) { final Loader currentLoader = mArchives.get(key); final Loader loader = mArchives.get(key); if (currentLoader == loader) { if (loader == null) { mArchives.remove(key); throw new IllegalStateException("Archive not acquired."); } } } } }); } mArchives.put(key, loader); return loader; return loader; } } } private static class Key { private static class Key { Uri archiveUri; Uri archiveUri; Loading
src/com/android/documentsui/archives/Loader.java +42 −19 Original line number Original line Diff line number Diff line Loading @@ -28,7 +28,6 @@ import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** /** * Loads an instance of Archive lazily. * Loads an instance of Archive lazily. Loading @@ -39,16 +38,19 @@ public class Loader { public static final int STATUS_OPENING = 0; public static final int STATUS_OPENING = 0; public static final int STATUS_OPENED = 1; public static final int STATUS_OPENED = 1; public static final int STATUS_FAILED = 2; 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 Context mContext; private final Uri mArchiveUri; private final Uri mArchiveUri; private final int mAccessMode; private final int mAccessMode; private final Uri mNotificationUri; private final Uri mNotificationUri; private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); private final Object mStatusLock = new Object(); private final Object mLock = new Object(); @GuardedBy("mStatusLock") @GuardedBy("mLock") private int mStatus = STATUS_OPENING; private int mStatus = STATUS_OPENING; @GuardedBy("mLock") private int mRefCount = 0; private Archive mArchive = null; private Archive mArchive = null; Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) { Loading @@ -62,19 +64,16 @@ public class Loader { } } synchronized Archive get() { synchronized Archive get() { synchronized (mStatusLock) { synchronized (mLock) { if (mStatus == STATUS_OPENED) { if (mStatus == STATUS_OPENED) { return mArchive; return mArchive; } } } } // Once loading the archive failed, do not to retry opening it until the synchronized (mLock) { // archive file has changed (the loader is deleted once we receive if (mStatus != STATUS_OPENING) { // a notification about the archive file being changed). synchronized (mStatusLock) { if (mStatus == STATUS_FAILED) { throw new IllegalStateException( 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."); } } } } Loading @@ -94,12 +93,18 @@ public class Loader { } else { } else { throw new IllegalStateException("Access mode not supported."); throw new IllegalStateException("Access mode not supported."); } } synchronized (mStatusLock) { boolean closedDueToRefcount = false; synchronized (mLock) { if (mRefCount == 0) { mArchive.close(); mStatus = STATUS_CLOSED; } else { mStatus = STATUS_OPENED; mStatus = STATUS_OPENED; } } } } catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) { Log.e(TAG, "Failed to open the archive.", e); Log.e(TAG, "Failed to open the archive.", e); synchronized (mStatusLock) { synchronized (mLock) { mStatus = STATUS_FAILED; mStatus = STATUS_FAILED; } } throw new IllegalStateException("Failed to open the archive.", e); throw new IllegalStateException("Failed to open the archive.", e); Loading @@ -115,16 +120,34 @@ public class Loader { } } int getStatus() { int getStatus() { synchronized (mStatusLock) { synchronized (mLock) { return mStatus; return mStatus; } } } } Lock getReadLock() { void acquire() { return mLock.readLock(); synchronized (mLock) { mRefCount++; } } } Lock getWriteLock() { void release() { return mLock.writeLock(); 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. } } } } } } }
src/com/android/documentsui/services/CompressJob.java +18 −3 Original line number Original line Diff line number Diff line Loading @@ -85,6 +85,10 @@ final class CompressJob extends CopyJob { @Override @Override public boolean setUp() { public boolean setUp() { if (!super.setUp()) { return false; } final ContentResolver resolver = appContext.getContentResolver(); final ContentResolver resolver = appContext.getContentResolver(); // TODO: Move this to DocumentsProvider. // TODO: Move this to DocumentsProvider. Loading @@ -95,20 +99,31 @@ final class CompressJob extends CopyJob { try { try { mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive( mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive( archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY)); archiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY)); ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri); } catch (FileNotFoundException e) { } catch (FileNotFoundException e) { Log.e(TAG, "Failed to create dstInfo.", e); Log.e(TAG, "Failed to create dstInfo.", e); failureCount = mResourceUris.getItemCount(); failureCount = mResourceUris.getItemCount(); return false; 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 @Override void finish() { void finish() { final ContentResolver resolver = appContext.getContentResolver(); try { ArchivesProvider.closeArchive(resolver, mDstInfo.derivedUri); 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. // TODO: Remove the archive file in case of an error. super.finish(); } } /** /** Loading