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

Commit 95cd85ad authored by Steve McKay's avatar Steve McKay
Browse files

Simplify State initialization w/ better semantics + improved Task security.

Also, reveal the illusion that we're restoring state in Files and Downloads.
Also, define a "PairedTask" class that guards calls to task methods
    with checks against isDestroyed. This also let us make all of the tasks
    static, so we get much narrower scope and tasks can even be moved to
    their own files.

Change-Id: I6a9e8706e1ab1d1f43301e73dd9858a115a6baaf
parent 3fd03b71
Loading
Loading
Loading
Loading
+41 −110
Original line number Diff line number Diff line
@@ -45,22 +45,16 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Spinner;
import android.widget.Toolbar;

import com.android.documentsui.RecentsProvider.ResumeColumns;
import com.android.documentsui.SearchManager.SearchManagerListener;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
import com.android.internal.util.Preconditions;

import libcore.io.IoUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -87,8 +81,8 @@ public abstract class BaseActivity extends Activity

    abstract void onTaskFinished(Uri... uris);
    abstract void refreshDirectory(int anim);
    abstract void saveStackBlocking();
    abstract State buildState();
    /** Allows sub-classes to include information in a newly created State instance. */
    abstract void includeState(State initialState);

    public BaseActivity(@LayoutRes int layoutId, String tag) {
        mLayoutId = layoutId;
@@ -103,9 +97,7 @@ public abstract class BaseActivity extends Activity
        setContentView(mLayoutId);

        mDrawer = DrawerController.create(this);
        mState = (icicle != null)
                ? icicle.<State>getParcelable(EXTRA_STATE)
                        : buildState();
        mState = getState(icicle);
        Metrics.logActivityLaunch(this, mState, getIntent());

        mRoots = DocumentsApplication.getRootsCache(this);
@@ -114,7 +106,8 @@ public abstract class BaseActivity extends Activity
                new RootsCache.OnCacheUpdateListener() {
                    @Override
                    public void onCacheUpdate() {
                        new HandleRootsChangedTask().execute(getCurrentRoot());
                        new HandleRootsChangedTask(BaseActivity.this)
                                .execute(getCurrentRoot());
                    }
                });

@@ -184,7 +177,20 @@ public abstract class BaseActivity extends Activity
        super.onDestroy();
    }

    State buildDefaultState() {
    private State getState(@Nullable Bundle icicle) {
        if (icicle != null) {
            State state = icicle.<State>getParcelable(EXTRA_STATE);
            if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
            return state;
        }

        State state = createSharedState();
        includeState(state);
        if (DEBUG) Log.d(mTag, "Created new state object: " + state);
        return state;
    }

    private State createSharedState() {
        State state = new State();

        final Intent intent = getIntent();
@@ -224,7 +230,7 @@ public abstract class BaseActivity extends Activity
        if (mRoots.isRecentsRoot(root)) {
            refreshCurrentRootAndDirectory(ANIM_NONE);
        } else {
            new PickRootTask(root).executeOnExecutor(getExecutorForCurrentDirectory());
            new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
        }
    }

@@ -574,115 +580,40 @@ public abstract class BaseActivity extends Activity
        }
    }

    final class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
    private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
        private RootInfo mRoot;

        public PickRootTask(RootInfo root) {
        public PickRootTask(BaseActivity activity, RootInfo root) {
            super(activity);
            mRoot = root;
        }

        @Override
        protected DocumentInfo doInBackground(Void... params) {
            return getRootDocumentBlocking(mRoot);
        }

        @Override
        protected void onPostExecute(DocumentInfo result) {
            if (result != null && !isDestroyed()) {
                openContainerDocument(result);
            }
        protected DocumentInfo run(Void... params) {
            return mOwner.getRootDocumentBlocking(mRoot);
        }
    }

    final class RestoreStackTask extends AsyncTask<Void, Void, Void> {
        private volatile boolean mRestoredStack;
        private volatile boolean mExternal;

        @Override
        protected Void doInBackground(Void... params) {
            if (DEBUG && !mState.stack.isEmpty()) {
                Log.w(mTag, "Overwriting existing stack.");
            }
            RootsCache roots = DocumentsApplication.getRootsCache(BaseActivity.this);

            // Restore last stack for calling package
            final String packageName = getCallingPackageMaybeExtra();
            final Cursor cursor = getContentResolver()
                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
            try {
                if (cursor.moveToFirst()) {
                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
                    final byte[] rawStack = cursor.getBlob(
                            cursor.getColumnIndex(ResumeColumns.STACK));
                    DurableUtils.readFromArray(rawStack, mState.stack);
                    mRestoredStack = true;
                }
            } catch (IOException e) {
                Log.w(mTag, "Failed to resume: " + e);
            } finally {
                IoUtils.closeQuietly(cursor);
        protected void finish(DocumentInfo result) {
            if (result != null) {
                mOwner.openContainerDocument(result);
            }

            if (mRestoredStack) {
                // Update the restored stack to ensure we have freshest data
                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
                try {
                    mState.stack.updateRoot(matchingRoots);
                    mState.stack.updateDocuments(getContentResolver());
                } catch (FileNotFoundException e) {
                    Log.w(mTag, "Failed to restore stack: " + e);
                    mState.stack.reset();
                    mRestoredStack = false;
        }
    }

            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (isDestroyed()) return;
            mState.restored = true;
            refreshCurrentRootAndDirectory(ANIM_NONE);
            onStackRestored(mRestoredStack, mExternal);
        }
    }

    final class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
        private Uri mRootUri;

        public RestoreRootTask(Uri rootUri) {
            mRootUri = rootUri;
        }

        @Override
        protected RootInfo doInBackground(Void... params) {
            final String rootId = DocumentsContract.getRootId(mRootUri);
            return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
        }

        @Override
        protected void onPostExecute(RootInfo root) {
            if (isDestroyed()) return;
            mState.restored = true;
    private static final class HandleRootsChangedTask
            extends PairedTask<BaseActivity, RootInfo, RootInfo> {
        DocumentInfo mHome;

            if (root != null) {
                onRootPicked(root);
            } else {
                Log.w(mTag, "Failed to find root: " + mRootUri);
                finish();
            }
        }
        public HandleRootsChangedTask(BaseActivity activity) {
            super(activity);
        }

    final class HandleRootsChangedTask extends AsyncTask<RootInfo, Void, RootInfo> {
        DocumentInfo mHome;

        @Override
        protected RootInfo doInBackground(RootInfo... roots) {
        protected RootInfo run(RootInfo... roots) {
            checkArgument(roots.length == 1);
            final RootInfo currentRoot = roots[0];
            final Collection<RootInfo> cachedRoots = mRoots.getRootsBlocking();
            final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
            RootInfo homeRoot = null;
            for (final RootInfo root : cachedRoots) {
                if (root.isHome()) {
@@ -694,17 +625,17 @@ public abstract class BaseActivity extends Activity
                }
            }
            Preconditions.checkNotNull(homeRoot);
            mHome = getRootDocumentBlocking(homeRoot);
            mHome = mOwner.getRootDocumentBlocking(homeRoot);
            return homeRoot;
        }

        @Override
        protected void onPostExecute(RootInfo homeRoot) {
            if (homeRoot != null && mHome != null && !isDestroyed()) {
        protected void finish(RootInfo homeRoot) {
            if (homeRoot != null && mHome != null) {
                // Clear entire backstack and start in new root
                mState.onRootChanged(homeRoot);
                mSearchManager.update(homeRoot);
                openContainerDocument(mHome);
                mOwner.mState.onRootChanged(homeRoot);
                mOwner.mSearchManager.update(homeRoot);
                mOwner.openContainerDocument(mHome);
            }
        }
    }
+113 −38
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.documentsui;

import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.State.ACTION_CREATE;
import static com.android.documentsui.State.ACTION_GET_CONTENT;
import static com.android.documentsui.State.ACTION_OPEN;
@@ -31,11 +32,12 @@ import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.DocumentsContract;
@@ -52,7 +54,12 @@ import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperationService;

import libcore.io.IoUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class DocumentsActivity extends BaseActivity {
@@ -94,16 +101,14 @@ public class DocumentsActivity extends BaseActivity {
            // In this case, we set the activity title in AsyncTask.onPostExecute().  To prevent
            // talkback from reading aloud the default title, we clear it here.
            setTitle("");
            new RestoreStackTask().execute();
            new RestoreStackTask(this).execute();
        } else {
            refreshCurrentRootAndDirectory(ANIM_NONE);
        }
    }

    @Override
    State buildState() {
        State state = buildDefaultState();

    void includeState(State state) {
        final Intent intent = getIntent();
        final String action = intent.getAction();
        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
@@ -134,8 +139,6 @@ public class DocumentsActivity extends BaseActivity {
            state.transferMode = intent.getIntExtra(FileOperationService.EXTRA_OPERATION,
                    FileOperationService.OPERATION_COPY);
        }

        return state;
    }

    @Override
@@ -319,11 +322,13 @@ public class DocumentsActivity extends BaseActivity {
    }

    void onSaveRequested(DocumentInfo replaceTarget) {
        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
        new ExistingFinishTask(this, replaceTarget.derivedUri)
                .executeOnExecutor(getExecutorForCurrentDirectory());
    }

    void onSaveRequested(String mimeType, String displayName) {
        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getExecutorForCurrentDirectory());
        new CreateFinishTask(this, mimeType, displayName)
                .executeOnExecutor(getExecutorForCurrentDirectory());
    }

    @Override
@@ -343,7 +348,8 @@ public class DocumentsActivity extends BaseActivity {
            openContainerDocument(doc);
        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
            // Explicit file picked, return
            new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
            new ExistingFinishTask(this, doc.derivedUri)
                    .executeOnExecutor(getExecutorForCurrentDirectory());
        } else if (mState.action == ACTION_CREATE) {
            // Replace selected file
            SaveFragment.get(fm).setReplaceTarget(doc);
@@ -358,7 +364,8 @@ public class DocumentsActivity extends BaseActivity {
            for (int i = 0; i < size; i++) {
                uris[i] = docs.get(i).derivedUri;
            }
            new ExistingFinishTask(uris).executeOnExecutor(getExecutorForCurrentDirectory());
            new ExistingFinishTask(this, uris)
                    .executeOnExecutor(getExecutorForCurrentDirectory());
        }
    }

@@ -373,11 +380,10 @@ public class DocumentsActivity extends BaseActivity {
            // Should not be reached.
            throw new IllegalStateException("Invalid mState.action.");
        }
        new PickFinishTask(result).executeOnExecutor(getExecutorForCurrentDirectory());
        new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory());
    }

    @Override
    void saveStackBlocking() {
    void writeStackToRecentsBlocking() {
        final ContentResolver resolver = getContentResolver();
        final ContentValues values = new ContentValues();

@@ -438,69 +444,138 @@ public class DocumentsActivity extends BaseActivity {
        finish();
    }


    public static DocumentsActivity get(Fragment fragment) {
        return (DocumentsActivity) fragment.getActivity();
    }

    private final class PickFinishTask extends AsyncTask<Void, Void, Void> {
    /**
     * Restores the stack from Recents for the specified package.
     */
    private static final class RestoreStackTask
            extends PairedTask<DocumentsActivity, Void, Void> {

        private volatile boolean mRestoredStack;
        private volatile boolean mExternal;
        private Context mContext;
        private State mState;

        public RestoreStackTask(DocumentsActivity activity) {
            super(activity);
            mState = activity.mState;
        }

        @Override
        protected Void run(Void... params) {
            if (DEBUG && !mState.stack.isEmpty()) {
                Log.w(TAG, "Overwriting existing stack.");
            }
            RootsCache roots = DocumentsApplication.getRootsCache(mContext);

            String packageName = mOwner.getCallingPackageMaybeExtra();
            Uri resumeUri = RecentsProvider.buildResume(packageName);
            Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null);
            try {
                if (cursor.moveToFirst()) {
                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
                    final byte[] rawStack = cursor.getBlob(
                            cursor.getColumnIndex(ResumeColumns.STACK));
                    DurableUtils.readFromArray(rawStack, mState.stack);
                    mRestoredStack = true;
                }
            } catch (IOException e) {
                Log.w(TAG, "Failed to resume: " + e);
            } finally {
                IoUtils.closeQuietly(cursor);
            }

            if (mRestoredStack) {
                // Update the restored stack to ensure we have freshest data
                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
                try {
                    mState.stack.updateRoot(matchingRoots);
                    mState.stack.updateDocuments(mOwner.getContentResolver());
                } catch (FileNotFoundException e) {
                    Log.w(TAG, "Failed to restore stack for package: " + packageName
                            + " because of error: "+ e);
                    mState.stack.reset();
                    mRestoredStack = false;
                }
            }

            return null;
        }

        @Override
        protected void finish(Void result) {
            mState.restored = true;
            mOwner.refreshCurrentRootAndDirectory(ANIM_NONE);
            mOwner.onStackRestored(mRestoredStack, mExternal);
        }
    }

    private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
        private final Uri mUri;

        public PickFinishTask(Uri uri) {
        public PickFinishTask(DocumentsActivity activity, Uri uri) {
            super(activity);
            mUri = uri;
        }

        @Override
        protected Void doInBackground(Void... params) {
            saveStackBlocking();
        protected Void run(Void... params) {
            mOwner.writeStackToRecentsBlocking();
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            onTaskFinished(mUri);
        protected void finish(Void result) {
            mOwner.onTaskFinished(mUri);
        }
    }

    final class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
    private static final class ExistingFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
        private final Uri[] mUris;

        public ExistingFinishTask(Uri... uris) {
        public ExistingFinishTask(DocumentsActivity activity, Uri... uris) {
            super(activity);
            mUris = uris;
        }

        @Override
        protected Void doInBackground(Void... params) {
            saveStackBlocking();
        protected Void run(Void... params) {
            mOwner.writeStackToRecentsBlocking();
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            onTaskFinished(mUris);
        protected void finish(Void result) {
            mOwner.onTaskFinished(mUris);
        }
    }

    /**
     * Task that creates a new document in the background.
     */
    final class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
    private static final class CreateFinishTask extends PairedTask<DocumentsActivity, Void, Uri> {
        private final String mMimeType;
        private final String mDisplayName;

        public CreateFinishTask(String mimeType, String displayName) {
        public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) {
            super(activity);
            mMimeType = mimeType;
            mDisplayName = displayName;
        }

        @Override
        protected void onPreExecute() {
            setPending(true);
        protected void prepare() {
            mOwner.setPending(true);
        }

        @Override
        protected Uri doInBackground(Void... params) {
            final ContentResolver resolver = getContentResolver();
            final DocumentInfo cwd = getCurrentDirectory();
        protected Uri run(Void... params) {
            final ContentResolver resolver = mOwner.getContentResolver();
            final DocumentInfo cwd = mOwner.getCurrentDirectory();

            ContentProviderClient client = null;
            Uri childUri = null;
@@ -516,22 +591,22 @@ public class DocumentsActivity extends BaseActivity {
            }

            if (childUri != null) {
                saveStackBlocking();
                mOwner.writeStackToRecentsBlocking();
            }

            return childUri;
        }

        @Override
        protected void onPostExecute(Uri result) {
        protected void finish(Uri result) {
            if (result != null) {
                onTaskFinished(result);
                mOwner.onTaskFinished(result);
            } else {
                Snackbars.makeSnackbar(
                    DocumentsActivity.this, R.string.save_error, Snackbar.LENGTH_SHORT).show();
                        mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show();
            }

            setPending(false);
            mOwner.setPending(false);
        }
    }
}
+2 −25
Original line number Diff line number Diff line
@@ -24,8 +24,6 @@ import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -37,10 +35,8 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toolbar;

import com.android.documentsui.RecentsProvider.ResumeColumns;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DurableUtils;
import com.android.documentsui.model.RootInfo;
import com.android.internal.util.Preconditions;

@@ -72,23 +68,19 @@ public class DownloadsActivity extends BaseActivity {
            // talkback from reading aloud the default title, we clear it here.
            setTitle("");
            final Uri rootUri = getIntent().getData();
            new RestoreRootTask(rootUri).executeOnExecutor(getExecutorForCurrentDirectory());
            new RestoreRootTask(this, rootUri).executeOnExecutor(getExecutorForCurrentDirectory());
        } else {
            refreshCurrentRootAndDirectory(ANIM_NONE);
        }
    }

    @Override
    State buildState() {
        State state = buildDefaultState();

    void includeState(State state) {
        state.action = ACTION_MANAGE;
        state.acceptMimes = new String[] { "*/*" };
        state.allowMultiple = true;
        state.showSize = true;
        state.excludedAuthorities = getExcludedAuthorities();

        return state;
    }

    @Override
@@ -169,21 +161,6 @@ public class DownloadsActivity extends BaseActivity {
    @Override
    public void onDocumentsPicked(List<DocumentInfo> docs) {}

    @Override
    void saveStackBlocking() {
        final ContentResolver resolver = getContentResolver();
        final ContentValues values = new ContentValues();

        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);

        // Remember location for next app launch
        final String packageName = getCallingPackageMaybeExtra();
        values.clear();
        values.put(ResumeColumns.STACK, rawStack);
        values.put(ResumeColumns.EXTERNAL, 0);
        resolver.insert(RecentsProvider.buildResume(packageName), values);
    }

    @Override
    void onTaskFinished(Uri... uris) {
        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
+24 −23

File changed.

Preview size limit exceeded, changes collapsed.

+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.documentsui;

import android.app.Activity;
import android.os.AsyncTask;

/**
 * An {@link AsyncTask} that guards work with checks that a paired {@link Activity}
 * is still alive. Instances of this class make no progress.
 *
 * <p>Use this type of task for greater safety when executing tasks that might complete
 * after an Activity is destroyed.
 *
 * <p>Also useful as tasks can be static, limiting scope, but still have access to
 * the owning class (by way the A template and the mActivity field).
 *
 * @template Owner Activity type.
 * @template Input input type
 * @template Output output type
 */
abstract class PairedTask<Owner extends Activity, Input, Output>
        extends AsyncTask<Input, Void, Output> {

    protected final Owner mOwner;

    public PairedTask(Owner owner) {
        mOwner = owner;
    }

    /** Called prior to run being executed. Analogous to {@link AsyncTask#onPreExecute} */
    void prepare() {}

    /** Analogous to {@link AsyncTask#doInBackground} */
    abstract Output run(Input... input);

    /** Analogous to {@link AsyncTask#onPostExecute} */
    abstract void finish(Output output);

    @Override
    final protected void onPreExecute() {
        if (mOwner.isDestroyed()) {
            return;
        }
        prepare();
    }

    @Override
    final protected Output doInBackground(Input... input) {
        if (mOwner.isDestroyed()) {
            return null;
        }
        return run(input);
    }

    @Override
    final protected void onPostExecute(Output result) {
        if (mOwner.isDestroyed()) {
            return;
        }
        finish(result);
    }
}
Loading