Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b19061c6 authored by Tomasz Mikolajewski's avatar Tomasz Mikolajewski
Browse files

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
parent 700f9716
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.os.RemoteException;
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 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
        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 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
            }

            cursor = mModel.sortCursor(cursor);

            result.client = client;
            result.cursor = cursor;
        } catch (Exception e) {
            Log.w(TAG, "Failed to query", e);
@@ -118,6 +121,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
            synchronized (this) {
                mSignal = null;
            }
            // TODO: Remove this call.
            ContentProviderClient.releaseQuietly(client);
        }

+5 −1
Original line number Diff line number Diff line
@@ -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 @@ public class DirectoryResult implements AutoCloseable {
    @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;
+97 −165
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ import android.provider.DocumentsContract;
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 @@ import java.util.concurrent.locks.Lock;
 * <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,9 +99,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
            @Nullable String sortOrder)
            throws FileNotFoundException {
        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        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) {
@@ -140,9 +128,6 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
        cursor.setNotificationUri(getContext().getContentResolver(),
                buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
        return cursor;
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
@@ -152,26 +137,14 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
            return Document.MIME_TYPE_DIR;
        }

        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        final Loader loader = getLoaderOrThrow(documentId);
        return loader.get().getDocumentType(documentId);
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
    public boolean isChildDocument(String parentDocumentId, String documentId) {
        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        final Loader loader = getLoaderOrThrow(documentId);
        return loader.get().isChildDocument(parentDocumentId, documentId);
        } catch (FileNotFoundException e) {
            throw new IllegalStateException(e);
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
@@ -201,52 +174,32 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
            }
        }

        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        final Loader loader = getLoaderOrThrow(documentId);
        return loader.get().queryDocument(documentId, projection);
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
    public String createDocument(
            String parentDocumentId, String mimeType, String displayName)
            throws FileNotFoundException {
        Loader loader = null;
        try {
            loader = obtainInstance(parentDocumentId);
        final Loader loader = getLoaderOrThrow(parentDocumentId);
        return loader.get().createDocument(parentDocumentId, mimeType, displayName);
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
    public ParcelFileDescriptor openDocument(
            String documentId, String mode, final CancellationSignal signal)
            throws FileNotFoundException {
        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        final Loader loader = getLoaderOrThrow(documentId);
        return loader.get().openDocument(documentId, mode, signal);
        } finally {
            releaseInstance(loader);
        }
    }

    @Override
    public AssetFileDescriptor openDocumentThumbnail(
            String documentId, Point sizeHint, final CancellationSignal signal)
            throws FileNotFoundException {
        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
        final Loader loader = getLoaderOrThrow(documentId);
        return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
        } finally {
            releaseInstance(loader);
        }
    }

    /**
@@ -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(),
                "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) {
        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
        synchronized (mArchives) {
            mArchives.remove(Key.fromArchiveId(archiveId));
    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);
        }
    }

    /**
     * Closes the helper and disposes all existing archives. It will block until all ongoing
     * operations on each opened archive are finished.
     * The archive won't close until all clients release it.
     */
    @Override
    // TODO: Wire close() to call().
    public void close() {
    private void acquireArchive(String documentId) {
        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
        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) {
            loader = getInstanceUncheckedLocked(documentId);
            loader.getReadLock().lock();
            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);
            }
        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;
        }

        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);
            final Loader loader = mArchives.get(key);
            if (loader == null) {
                throw new IllegalStateException("Archive not acquired.");
            }
                            }
                        }
                    });
        }

        mArchives.put(key, loader);
            return loader;
        }
    }

    private static class Key {
        Uri archiveUri;
+42 −19
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import java.io.IOException;
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 class Loader {
    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 @@ public class Loader {
    }

    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 @@ public class Loader {
            } else {
                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;
                }
            }
        } 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 @@ public class Loader {
    }

    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.
                }
            }
        }
    }
}
+18 −3
Original line number Diff line number Diff line
@@ -85,6 +85,10 @@ final class CompressJob extends CopyJob {

    @Override
    public boolean setUp() {
        if (!super.setUp()) {
            return false;
        }

        final ContentResolver resolver = appContext.getContentResolver();

        // TODO: Move this to DocumentsProvider.
@@ -95,20 +99,31 @@ final class CompressJob extends CopyJob {
        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();
    }

    /**
Loading