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

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

Load archives in background.

ArchiveProvider's queryChildDocuments used to be synchronous, which
would for large archives may take seconds and timeout.

This CL moves loading to another thread and utilizes EXTRA_LOADING so
DocumentsUI has a chance to show a spinner when loading large archives.

Bug: 31783726
Test: Open a ZIP file containing Linux kernel.

Change-Id: I91a97a12e22db71db77d5d06d66d108a03622c3f
parent 1c9e386f
Loading
Loading
Loading
Loading
+43 −8
Original line number Diff line number Diff line
@@ -21,10 +21,11 @@ import android.content.res.AssetFileDescriptor;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
@@ -44,6 +45,8 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -95,9 +98,23 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
            @Nullable String sortOrder)
            throws FileNotFoundException {
        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
        Loader loader = null;
        try {
            loader = obtainInstance(documentId);
            if (loader.mArchive == null) {
                final MatrixCursor cursor = new MatrixCursor(
                        projection != null ? projection : Archive.DEFAULT_PROJECTION);
                // 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.
                final Bundle bundle = new Bundle();
                bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
                cursor.setExtras(bundle);
                cursor.setNotificationUri(getContext().getContentResolver(),
                        buildUriForArchive(archiveId.mArchiveUri));
                return cursor;
            }
            return loader.get().queryChildDocuments(documentId, projection, sortOrder);
        } finally {
            releaseInstance(loader);
@@ -138,16 +155,11 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
            throws FileNotFoundException {
        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
        if (archiveId.mPath.equals("/")) {
            // For the archive's root directory return hard-coded cursor, so clients know that
            // it's actually a directory and queryChildDocuments() can be called on it.
            //
            // TODO: Move this code to the Archive class, once opening archives is moved to
            // background.
            final MatrixCursor cursor = new MatrixCursor(
                    projection != null ? projection : Archive.DEFAULT_PROJECTION);
            final RowBuilder row = cursor.newRow();
            row.add(Document.COLUMN_DOCUMENT_ID, documentId);
            row.add(Document.COLUMN_DISPLAY_NAME, "Archive");  // TODO: Fix.
            row.add(Document.COLUMN_DISPLAY_NAME, "Archive");  // TODO: Translate.
            row.add(Document.COLUMN_SIZE, 0);
            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
            return cursor;
@@ -278,12 +290,17 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
        private final Uri mArchiveUri;
        private final Uri mNotificationUri;
        private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
        private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
        private Archive mArchive = null;
        private Exception mFailureException = null;

        Loader(Context context, Uri archiveUri, Uri notificationUri) {
            this.mContext = context;
            this.mArchiveUri = archiveUri;
            this.mNotificationUri = notificationUri;

            // Start loading the archive immediately in the background.
            mExecutor.submit(this::get);
        }

        synchronized Archive get() throws FileNotFoundException {
@@ -291,6 +308,15 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
                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).
            if (mFailureException != null) {
                throw new IllegalStateException(
                        "Trying to perform an operation on an archive which failed to load.",
                        mFailureException);
            }

            try {
                mArchive = Archive.createForParcelFileDescriptor(
                        mContext,
@@ -298,9 +324,18 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
                                mArchiveUri, "r", null /* signal */),
                        mArchiveUri, mNotificationUri);
            } catch (IOException e) {
                mFailureException = e;
                throw new IllegalStateException(e);
            } catch (RuntimeException e) {
                mFailureException = e;
                throw e;
            } finally {
                // Notify observers that the root directory is loaded (or failed)
                // so clients reload it.
                mContext.getContentResolver().notifyChange(
                        buildUriForArchive(mArchiveUri),
                        null /* observer */, false /* syncToNetwork */);
            }

            return mArchive;
        }