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

Commit 2bab2f8e authored by Steve McKay's avatar Steve McKay
Browse files

Preserve selection in memory during rotation.

Using onRetainNonConfigurationInstance
Eliminate 1k limit on selections.
Bug: 28194201

Change-Id: Ic18e441dad418c2b829abd01f8ad61fc1e4d3391
parent 19cf55b1
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
@@ -247,9 +247,4 @@
        <item quantity="one">Delete <xliff:g id="count" example="1">%1$d</xliff:g> item?</item>
        <item quantity="other">Delete <xliff:g id="count" example="3">%1$d</xliff:g> items?</item>
    </plurals>
    <!-- Snackbar shown to users who wanted to select more than 1000 items (files or directories). -->
    <string name="too_many_selected">Sorry, you can only select up to 1000 items at a time</string>
    <!-- Snackbar shown to users who wanted to select all, but there were too many items (files or directories).
         Only the first 1000 items are selected in such case. -->
    <string name="too_many_in_select_all">Could only select 1000 items</string>
</resources>
+23 −0
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ public abstract class BaseActivity extends Activity
    private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";

    State mState;
    @Nullable RetainedState mRetainedState;
    RootsCache mRoots;
    SearchViewManager mSearchManager;
    DrawerController mDrawer;
@@ -123,6 +124,10 @@ public abstract class BaseActivity extends Activity
        mState = getState(icicle);
        Metrics.logActivityLaunch(this, mState, intent);

        // we're really interested in retainining state in our very complex
        // DirectoryFragment. So we do a little code yoga to extend
        // support to that fragment.
        mRetainedState = (RetainedState) getLastNonConfigurationInstance();
        mRoots = DocumentsApplication.getRootsCache(this);

        getContentResolver().registerContentObserver(
@@ -578,6 +583,24 @@ public abstract class BaseActivity extends Activity
        super.onRestoreInstanceState(state);
    }

    /**
     * Delegate ths call to the current fragment so it can save selection.
     * Feel free to expand on this with other useful state.
     */
    @Override
    public RetainedState onRetainNonConfigurationInstance() {
        RetainedState retained = new RetainedState();
        DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
        if (fragment != null) {
            fragment.retainState(retained);
        }
        return retained;
    }

    public @Nullable RetainedState getRetainedState() {
        return mRetainedState;
    }

    @Override
    public boolean isSearchExpanded() {
        return mSearchManager.isExpanded();
+36 −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.support.annotation.Nullable;

import com.android.documentsui.dirlist.MultiSelectManager.Selection;

/**
 * Object used to collect retained state from activity and fragments. Used
 * with Activity#onRetainNonConfigurationInstance. Information stored in
 * this class should be primarily ephemeral as instances of the class
 * only last across configuration changes (like device rotation). When
 * an application is fully town down, all instances are lost, fa-evah!
 */
public final class RetainedState {
    public @Nullable Selection selection;

    public boolean hasSelection() {
        return selection != null;
    }
}
+50 −63
Original line number Diff line number Diff line
@@ -17,16 +17,14 @@
package com.android.documentsui.dirlist;

import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.MAX_DOCS_IN_INTENT;
import static com.android.documentsui.State.MODE_GRID;
import static com.android.documentsui.State.MODE_LIST;
import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorString;

import com.google.common.collect.Lists;

import android.annotation.IntDef;
import android.annotation.StringRes;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
@@ -47,12 +45,9 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v13.view.DragStartHelper;
import android.support.v7.widget.GridLayoutManager;
@@ -96,6 +91,7 @@ import com.android.documentsui.Metrics;
import com.android.documentsui.MimePredicate;
import com.android.documentsui.R;
import com.android.documentsui.RecentsLoader;
import com.android.documentsui.RetainedState;
import com.android.documentsui.RootsCache;
import com.android.documentsui.Shared;
import com.android.documentsui.Snackbars;
@@ -109,14 +105,16 @@ import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;

import com.google.common.collect.Lists;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Nullable;

/**
 * Display the documents inside a single directory.
@@ -174,8 +172,9 @@ public class DirectoryFragment extends Fragment
    private RootInfo mRoot;
    private DocumentInfo mDocument;
    private String mQuery = null;
    // Save selection found during creation so it can be restored during directory loading.
    private Selection mSelection = null;
    // Note, we use !null to indicate that selection was restored (from rotation).
    // So don't fiddle with this field unless you've got the bigger picture in mind.
    private @Nullable Selection mRestoredSelection = null;
    private boolean mSearchMode = false;

    private @Nullable BandController mBandController;
@@ -267,10 +266,17 @@ public class DirectoryFragment extends Fragment
        mStateKey = buildStateKey(mRoot, mDocument);
        mQuery = args.getString(Shared.EXTRA_QUERY);
        mType = args.getInt(Shared.EXTRA_TYPE);
        final Selection selection = args.getParcelable(Shared.EXTRA_SELECTION);
        mSelection = selection != null ? selection : new Selection();
        mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);

        // Restore any selection we may have squirreled away in retained state.
        @Nullable RetainedState retained = getBaseActivity().getRetainedState();
        if (retained != null && retained.hasSelection()) {
            // We claim the selection for ourselves and null it out once used
            // so we don't have a rando selection hanging around in RetainedState.
            mRestoredSelection = retained.selection;
            retained.selection = null;
        }

        mIconHelper = new IconHelper(context, MODE_GRID);

        mAdapter = new SectionBreakDocumentsAdapterWrapper(
@@ -326,29 +332,18 @@ public class DirectoryFragment extends Fragment
        getLoaderManager().restartLoader(LOADER_ID, null, this);
    }

    public void retainState(RetainedState state) {
        state.selection = mSelectionManager.getSelection(new Selection());
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        mSelectionManager.getSelection(mSelection);

        outState.putInt(Shared.EXTRA_TYPE, mType);
        outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
        outState.putParcelable(Shared.EXTRA_DOC, mDocument);
        outState.putString(Shared.EXTRA_QUERY, mQuery);

        // Workaround. To avoid crash, write only up to 512 KB of selection.
        // If more files are selected, then the selection will be lost.
        final Parcel parcel = Parcel.obtain();
        try {
            mSelection.writeToParcel(parcel, 0);
            if (parcel.dataSize() <= 512 * 1024) {
                outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
            }
        } finally {
            parcel.recycle();
        }

        outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
    }

@@ -405,7 +400,7 @@ public class DirectoryFragment extends Fragment
        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
        if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
            ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
            getBaseActivity().onDocumentPicked(doc, mModel);
            mSelectionManager.clearSelection();
            return true;
        }
@@ -497,6 +492,12 @@ public class DirectoryFragment extends Fragment
        return mColumnCount;
    }

    // Support method to replace getOwner().foo() with something
    // slightly less clumsy like: getOwner().foo().
    private BaseActivity getBaseActivity() {
        return (BaseActivity) getActivity();
    }

    /**
     * Manages the integration between our ActionMode and MultiSelectManager, initiating
     * ActionMode when there is a selection, canceling it when there is no selection,
@@ -529,15 +530,7 @@ public class DirectoryFragment extends Fragment
                if (!mTuner.canSelectType(docMimeType, docFlags)) {
                    return false;
                }

                if (mSelected.size() >= MAX_DOCS_IN_INTENT) {
                    Snackbars.makeSnackbar(
                            getActivity(),
                            R.string.too_many_selected,
                            Snackbar.LENGTH_SHORT)
                            .show();
                    return false;
                }
                return mTuner.canSelectType(docMimeType, docFlags);
            }
            return true;
        }
@@ -624,7 +617,15 @@ public class DirectoryFragment extends Fragment

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            if (mRestoredSelection != null) {
                // This is a careful little song and dance to avoid haptic feedback
                // when selection has been restored after rotation. We're
                // also responsible for cleaning up restored selection so the
                // object dones't unnecessarily hang around.
                mRestoredSelection = null;
            } else {
                mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            }

            int size = mSelectionManager.getSelection().size();
            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
@@ -990,7 +991,7 @@ public class DirectoryFragment extends Fragment

    @Override
    public State getDisplayState() {
        return ((BaseActivity) getActivity()).getDisplayState();
        return getBaseActivity().getDisplayState();
    }

    @Override
@@ -1120,17 +1121,9 @@ public class DirectoryFragment extends Fragment
    public void selectAllFiles() {
        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL);

        // Exclude disabled files.
        Set<String> enabled = new HashSet<String>();
        List<String> modelIds = mAdapter.getModelIds();

        // Get the current selection.
        String[] alreadySelected = mSelectionManager.getSelection().getAll();
        for (String id : alreadySelected) {
           enabled.add(id);
        }

        for (String id : modelIds) {
        // Exclude disabled files
        List<String> enabled = new ArrayList<String>();
        for (String id : mAdapter.getModelIds()) {
            Cursor cursor = getModel().getItem(id);
            if (cursor == null) {
                Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
@@ -1138,15 +1131,7 @@ public class DirectoryFragment extends Fragment
            }
            String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
            int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
            if (mTuner.canSelectType(docMimeType, docFlags)) {
                if (enabled.size() >= MAX_DOCS_IN_INTENT) {
                    Snackbars.makeSnackbar(
                        getActivity(),
                        R.string.too_many_in_select_all,
                        Snackbar.LENGTH_SHORT)
                        .show();
                    break;
                }
            if (isDocumentEnabled(docMimeType, docFlags)) {
                enabled.add(id);
            }
        }
@@ -1528,7 +1513,7 @@ public class DirectoryFragment extends Fragment
            }

            if (!model.isLoading()) {
                ((BaseActivity) getActivity()).notifyDirectoryLoaded(
                getBaseActivity().notifyDirectoryLoaded(
                    model.doc != null ? model.doc.derivedUri : null);
            }
        }
@@ -1789,9 +1774,11 @@ public class DirectoryFragment extends Fragment

        updateLayout(state.derivedMode);

        if (mSelection != null) {
            mSelectionManager.setItemsSelected(mSelection.toList(), true);
            mSelection.clear();
        if (mRestoredSelection != null) {
            mSelectionManager.setItemsSelected(mRestoredSelection.toList(), true);
            // Note, we'll take care of cleaning up retained selection
            // in the selection handler where we already have some
            // specialized code to handle when selection was restored.
        }

        // Restore any previous instance state