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

Commit c5ecf890 authored by Steve McKay's avatar Steve McKay
Browse files

Move Adapters to their own classes.

Move section break support into a separate wrapper class.
Fix issue where intermediate directory updates were briefly showing hidden files.
Add a rudimentary test for ModelBackedDocumentsAdapter.

Bug: 26293561, 26383237, 26293561, 26309025
Change-Id: I1fa489b110754d8801091b2009caebe9d2278701
parent cdbbbe05
Loading
Loading
Loading
Loading
+47 −246
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
@@ -99,18 +100,17 @@ import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * Display the documents inside a single directory.
 */
public class DirectoryFragment extends Fragment {
public class DirectoryFragment extends Fragment implements DocumentsAdapter.Environment {

    public static final int TYPE_NORMAL = 1;
    public static final int TYPE_SEARCH = 2;
@@ -126,7 +126,7 @@ public class DirectoryFragment extends Fragment {
    private static final String TAG = "DirectoryFragment";

    private static final int LOADER_ID = 42;
    private static final boolean DEBUG_ENABLE_DND = true;
    static final boolean DEBUG_ENABLE_DND = true;

    private static final String EXTRA_TYPE = "type";
    private static final String EXTRA_ROOT = "root";
@@ -289,7 +289,11 @@ public class DirectoryFragment extends Fragment {
        final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
        final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);

        mAdapter = new DocumentsAdapter();
        mIconHelper = new IconHelper(context, state.derivedMode);

        mAdapter = new SectionBreakDocumentsAdapterWrapper(
                this, new ModelBackedDocumentsAdapter(this, mIconHelper));

        mRecView.setAdapter(mAdapter);

        GestureDetector.SimpleOnGestureListener listener =
@@ -333,7 +337,7 @@ public class DirectoryFragment extends Fragment {
                    : MultiSelectManager.MODE_SINGLE);
        mSelectionManager.addCallback(new SelectionModeListener());

        mModel = new Model(context, mAdapter);
        mModel = new Model(context);
        mModel.addUpdateListener(mAdapter);
        mModel.addUpdateListener(mModelUpdateListener);

@@ -343,8 +347,6 @@ public class DirectoryFragment extends Fragment {
        mTuner = FragmentTuner.pick(state);
        mClipper = new DocumentClipper(context);

        mIconHelper = new IconHelper(context, state.derivedMode);

        boolean hideGridTitles;
        if (mType == TYPE_RECENT_OPEN) {
            // Hide titles when showing recents for picking images/videos
@@ -574,7 +576,10 @@ public class DirectoryFragment extends Fragment {
            case MODE_GRID:
                if (mGridLayout == null) {
                    mGridLayout = new GridLayoutManager(getContext(), mColumnCount);
                    mGridLayout.setSpanSizeLookup(mAdapter.createSpanSizeLookup());
                    SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
                    if (lookup != null) {
                        mGridLayout.setSpanSizeLookup(lookup);
                    }
                }
                layout = mGridLayout;
                break;
@@ -609,6 +614,11 @@ public class DirectoryFragment extends Fragment {
        return columnCount;
    }

    @Override
    public int getColumnCount() {
        return mColumnCount;
    }

    /**
     * Manages the integration between our ActionMode and MultiSelectManager, initiating
     * ActionMode when there is a selection, canceling it when there is no selection,
@@ -893,265 +903,55 @@ public class DirectoryFragment extends Fragment {
        }.execute(selected);
    }

    private State getDisplayState() {
        return ((BaseActivity) getActivity()).getDisplayState();
    }

    void showEmptyView() {
        mEmptyView.setVisibility(View.VISIBLE);
        mRecView.setVisibility(View.GONE);
        TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
        msg.setText(R.string.empty);
        // No retry button for the empty view.
        mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
    }

    void showErrorView() {
        mEmptyView.setVisibility(View.VISIBLE);
        mRecView.setVisibility(View.GONE);
        TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
        msg.setText(R.string.query_error);
        // TODO: Enable this once the retry button does something.
        mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
    }

    void showRecyclerView() {
        mEmptyView.setVisibility(View.GONE);
        mRecView.setVisibility(View.VISIBLE);
    }

    final class DocumentsAdapter
            extends RecyclerView.Adapter<DocumentHolder>
            implements Model.UpdateListener {

        private static final String TAG = "DocumentsAdapter";
        public static final int ITEM_TYPE_LAYOUT_DIVIDER = 0;
        public static final int ITEM_TYPE_DOCUMENT = 1;
        public static final int ITEM_TYPE_DIRECTORY = 2;

        /**
         * An ordered list of model IDs. This is the data structure that determines what shows up in
         * the UI, and where.
         */
        private List<String> mModelIds = new ArrayList<>();

        // The list is divided into two segments - directories, and everything else. Record the
        // position where the transition happens.
        private int mDividerPosition;

        public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
            return new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    // Make layout whitespace span the grid. This has the effect of breaking
                    // grid rows whenever layout whitespace is encountered.
                    if (getItemViewType(position) == ITEM_TYPE_LAYOUT_DIVIDER) {
                        return mColumnCount;
                    } else {
                        return 1;
                    }
                }
            };
        }

    @Override
        public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == ITEM_TYPE_LAYOUT_DIVIDER) {
                return new EmptyDocumentHolder(getContext());
            };

            DocumentHolder holder = null;
            final State state = getDisplayState();
            switch (state.derivedMode) {
                case MODE_GRID:
                    switch (viewType) {
                        case ITEM_TYPE_DIRECTORY:
                            holder = new GridDirectoryHolder(getContext(), parent);
                            break;
                        case ITEM_TYPE_DOCUMENT:
                            holder = new GridDocumentHolder(getContext(), parent, mIconHelper);
                            break;
                        default:
                            throw new IllegalStateException("Unsupported layout type.");
                    }
                    break;
                case MODE_LIST:
                    holder = new ListDocumentHolder(getContext(), parent, mIconHelper);
                    break;
                case MODE_UNKNOWN:
                default:
                    throw new IllegalStateException("Unsupported layout mode.");
            }

    public void initDocumentHolder(DocumentHolder holder) {
        holder.addClickListener(mItemClickListener);
        holder.addOnKeyListener(mSelectionManager);
            return holder;
        }

        /**
         * Deal with selection changed events by using a custom ItemAnimator that just changes the
         * background color.  This works around focus issues (otherwise items lose focus when their
         * selection state changes) but also optimizes change animations for selection.
         */
        @Override
        public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) {
            if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) {
                // Whitespace items are hidden elements with no data to bind.
                return;
            }

            final View itemView = holder.itemView;

            if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) {
                final boolean selected = isSelected(mModelIds.get(position));
                itemView.setActivated(selected);
                return;
            } else {
                onBindViewHolder(holder, position);
            }
    }

    @Override
        public void onBindViewHolder(DocumentHolder holder, int position) {
            if (holder.getItemViewType() == ITEM_TYPE_LAYOUT_DIVIDER) {
                // Whitespace items are hidden elements with no data to bind.
                return;
            }

            String modelId = mModelIds.get(position);
            Cursor cursor = mModel.getItem(modelId);
            holder.bind(cursor, modelId, getDisplayState());

            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);

            holder.setSelected(isSelected(modelId));
            holder.setEnabled(mTuner.isDocumentEnabled(docMimeType, docFlags));
    public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
        if (DEBUG_ENABLE_DND) {
            setupDragAndDropOnDocumentView(holder.itemView, cursor);
        }
    }

    @Override
        public int getItemCount() {
            return mModelIds.size();
    public State getDisplayState() {
        return ((BaseActivity) getActivity()).getDisplayState();
    }

    @Override
        public void onModelUpdate(Model model) {
            mModelIds = Lists.newArrayList(model.getModelIds());
            // Start the divider at the end. That way if the code below encounters no documents
            // (i.e. in a directory containing only directories), the divider is placed at the end
            // of the list, as expected.
            mDividerPosition = mModelIds.size();

            // Walk down the list of IDs till we encounter something that's not a directory, and
            // insert a whitespace element - this introduces a visual break in the grid between
            // folders and documents.
            // TODO: This code makes assumptions about the model, namely, that it performs a
            // bucketed sort where directories will always be ordered before other files.  CBB.
            for (int i = 0; i < mModelIds.size(); ++i) {
                final String mimeType = getCursorString(
                        model.getItem(mModelIds.get(i)), Document.COLUMN_MIME_TYPE);
                if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
                    mDividerPosition = i;
                    break;
                }
            }

            mModelIds.add(mDividerPosition, null);
    public Model getModel() {
        return mModel;
    }

    @Override
        public void onModelUpdateFailed(Exception e) {
            if (DEBUG) Log.d(TAG, "onModelUpdateFailed called ");
            mModelIds.clear();
        }

        /**
         * @return The model ID of the item at the given adapter position.
         */
        public String getModelId(int adapterPosition) {
            return mModelIds.get(adapterPosition);
        }

        /**
         * Hides a set of items from the associated RecyclerView.
         *
         * @param ids The Model IDs of the items to hide.
         * @return A SparseArray that maps the hidden IDs to their old positions. This can be used
         *         to {@link #unhide} the items if necessary.
         */
        public SparseArray<String> hide(String... ids) {
            Set<String> toHide = Sets.newHashSet(ids);

            // Proceed backwards through the list of items, because each removal causes the
            // positions of all subsequent items to change.
            SparseArray<String> hiddenItems = new SparseArray<>();
            for (int i = mModelIds.size() - 1; i >= 0; --i) {
                String id = mModelIds.get(i);
                if (toHide.contains(id)) {
                    hiddenItems.put(i, mModelIds.remove(i));
                    notifyItemRemoved(i);
                }
    public boolean isDocumentEnabled(String docMimeType, int docFlags) {
        return mTuner.isDocumentEnabled(docMimeType, docFlags);
    }

            return hiddenItems;
        }

        /**
         * Unhides a set of previously hidden items.
         *
         * @param ids A sparse array of IDs from a previous call to {@link #hide}.
         */
        public void unhide(SparseArray<String> ids) {
            // Proceed backwards through the list of items, because each addition causes the
            // positions of all subsequent items to change.
            for (int i = ids.size() - 1; i >= 0; --i) {
                int pos = ids.keyAt(i);
                String id = ids.get(pos);
                mModelIds.add(pos, id);
                notifyItemInserted(pos);
            }
        }

        /**
         * Returns a list of model IDs of items currently in the adapter. Excludes items that are
         * currently hidden (see {@link #hide(String...)}).
         *
         * @return A list of Model IDs.
         */
        public List<String> getModelIds() {
            return mModelIds;
    void showEmptyView() {
        mEmptyView.setVisibility(View.VISIBLE);
        mRecView.setVisibility(View.GONE);
        TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
        msg.setText(R.string.empty);
        // No retry button for the empty view.
        mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
    }

        @Override
        public int getItemViewType(int position) {
            if (position < mDividerPosition) {
                return ITEM_TYPE_DIRECTORY;
            } else if (position == mDividerPosition) {
                return ITEM_TYPE_LAYOUT_DIVIDER;
            } else {
                return ITEM_TYPE_DOCUMENT;
            }
    void showErrorView() {
        mEmptyView.setVisibility(View.VISIBLE);
        mRecView.setVisibility(View.GONE);
        TextView msg = (TextView) mEmptyView.findViewById(R.id.message);
        msg.setText(R.string.query_error);
        // TODO: Enable this once the retry button does something.
        mEmptyView.findViewById(R.id.button_retry).setVisibility(View.GONE);
    }

        /**
         * Triggers item-change notifications by stable ID. Passing an unrecognized ID will result
         * in a warning in logcat, but no other error.
         *
         * @param id
         * @param selectionChangedMarker
         */
        public void notifyItemChanged(String id, String selectionChangedMarker) {
            int position = mModelIds.indexOf(id);

            if (position >= 0) {
                notifyItemChanged(position, selectionChangedMarker);
            } else {
                Log.w(TAG, "Item change notification received for unknown item: " + id);
            }
        }
    void showRecyclerView() {
        mEmptyView.setVisibility(View.GONE);
        mRecView.setVisibility(View.VISIBLE);
    }

    private String findCommonMimeType(List<String> mimeTypes) {
@@ -1504,7 +1304,8 @@ public class DirectoryFragment extends Fragment {
        abstract void onDocumentsReady(List<DocumentInfo> docs);
    }

    boolean isSelected(String modelId) {
    @Override
    public boolean isSelected(String modelId) {
        return mSelectionManager.getSelection().contains(modelId);
    }

@@ -1520,7 +1321,7 @@ public class DirectoryFragment extends Fragment {
        }
    }

    private class ModelUpdateListener implements Model.UpdateListener {
    private final class ModelUpdateListener implements Model.UpdateListener {
        @Override
        public void onModelUpdate(Model model) {
            if (model.info != null || model.error != null) {
+117 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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.dirlist;

import static com.android.documentsui.model.DocumentInfo.getCursorString;

import android.content.Context;
import android.database.Cursor;
import android.provider.DocumentsContract.Document;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;

import com.android.documentsui.State;

import java.nio.channels.UnsupportedAddressTypeException;
import java.util.List;

/**
 * DocumentsAdapter provides glue between a directory Model, and RecylcerView. We've
 * abstracted this a bit in order to decompose some specialized support
 * for adding dummy layout objects (@see SectionBreakDocumentsAdapter). Handling of the
 * dummy layout objects was error prone when interspersed with the core mode / adapter code.
 *
 * @see ModelBackedDocumentsAdapter
 * @see SectionBreakDocumentsAdapter
 */
abstract class DocumentsAdapter
        extends RecyclerView.Adapter<DocumentHolder>
        implements Model.UpdateListener {

    // Payloads for notifyItemChange to distinguish between selection and other events.
    static final String SELECTION_CHANGED_MARKER = "Selection-Changed";

    /**
     * Returns a list of model IDs of items currently in the adapter. Excludes items that are
     * currently hidden (see {@link #hide(String...)}).
     *
     * @return A list of Model IDs.
     */
    abstract List<String> getModelIds();

    /**
     * Triggers item-change notifications by stable ID (as opposed to position).
     * Passing an unrecognized ID will result in a warning in logcat, but no other error.
     */
    abstract void notifyItemSelectionChanged(String id);

    /**
     * @return The model ID of the item at the given adapter position.
     */
    abstract String getModelId(int position);

    /**
     * Hides a set of items from the associated RecyclerView.
     *
     * @param ids The Model IDs of the items to hide.
     * @return A SparseArray that maps the hidden IDs to their old positions. This can be used
     *         to {@link #unhide} the items if necessary.
     */
    abstract public SparseArray<String> hide(String... ids);

    /**
     * Unhides a set of previously hidden items.
     *
     * @param ids A sparse array of IDs from a previous call to {@link #hide}.
     */
    abstract void unhide(SparseArray<String> ids);

    /**
     * Returns a class that yields the span size for a particular element. This is
     * primarily useful in {@link SectionBreakDocumentsAdapterWrapper} where
     * we adjust sizes.
     */
    GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
        throw new UnsupportedAddressTypeException();
    }

    static boolean isDirectory(Cursor cursor) {
        final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
        return Document.MIME_TYPE_DIR.equals(mimeType);
    }

    boolean isDirectory(Model model, int position) {
        String modelId = getModelIds().get(position);
        Cursor cursor = model.getItem(modelId);
        return isDirectory(cursor);
    }

    /**
     * Environmental access for View adapter implementations.
     */
    interface Environment {
        Context getContext();
        int getColumnCount();
        State getDisplayState();
        boolean isSelected(String id);
        Model getModel();
        boolean isDocumentEnabled(String mimeType, int flags);
        void initDocumentHolder(DocumentHolder holder);
        void onBindDocumentHolder(DocumentHolder holder, Cursor cursor);
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import com.android.documentsui.BaseActivity.SiblingProvider;
@@ -74,7 +73,7 @@ public class Model implements SiblingProvider {
    @Nullable String info;
    @Nullable String error;

    Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
    Model(Context context) {
        mContext = context;
    }

+223 −0

File added.

Preview size limit exceeded, changes collapsed.

+22 −10
Original line number Diff line number Diff line
@@ -75,9 +75,6 @@ public final class MultiSelectManager implements View.OnKeyListener {

    private boolean mSingleSelect;

    // Payloads for notifyItemChange to distinguish between selection and other events.
    public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";

    @Nullable private BandController mBandManager;

    /**
@@ -339,7 +336,10 @@ public final class MultiSelectManager implements View.OnKeyListener {
            if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
            return;
        }
        toggleSelection(mEnvironment.getModelIdFromAdapterPosition(position));
        String id = mEnvironment.getModelIdFromAdapterPosition(position);
        if (id != null) {
            toggleSelection(id);
        }
    }

    /**
@@ -348,6 +348,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
     * @param modelId
     */
    public void toggleSelection(String modelId) {
        checkNotNull(modelId);
        boolean changed = false;
        if (mSelection.contains(modelId)) {
            changed = attemptDeselect(modelId);
@@ -405,6 +406,10 @@ public final class MultiSelectManager implements View.OnKeyListener {
        checkState(end >= begin);
        for (int i = begin; i <= end; i++) {
            String id = mEnvironment.getModelIdFromAdapterPosition(i);
            if (id == null) {
                continue;
            }

            if (selected) {
                boolean canSelect = notifyBeforeItemStateChange(id, true);
                if (canSelect) {
@@ -436,6 +441,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
     * @return True if the update was applied.
     */
    private boolean attemptDeselect(String id) {
        checkArgument(id != null);
        if (notifyBeforeItemStateChange(id, false)) {
            mSelection.remove(id);
            notifyItemStateChanged(id, false);
@@ -462,6 +468,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
     * (identified by {@code position}) changes.
     */
    private void notifyItemStateChanged(String id, boolean selected) {
        checkArgument(id != null);
        int lastListener = mCallbacks.size() - 1;
        for (int i = lastListener; i > -1; i--) {
            mCallbacks.get(i).onItemStateChanged(id, selected);
@@ -613,7 +620,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
         * @param id
         * @return true if the position is currently selected.
         */
        public boolean contains(String id) {
        public boolean contains(@Nullable String id) {
            return mTotalSelection.contains(id);
        }

@@ -804,7 +811,12 @@ public final class MultiSelectManager implements View.OnKeyListener {
        int getChildCount();
        int getVisibleChildCount();
        void focusItem(int position);
        String getModelIdFromAdapterPosition(int position);
        /**
         * Returns null if non-useful item.
         * @param position
         * @return
         */
        @Nullable String getModelIdFromAdapterPosition(int position);
        int getItemCount();
        List<String> getModelIds();
        void notifyItemChanged(String id);
@@ -818,11 +830,11 @@ public final class MultiSelectManager implements View.OnKeyListener {
        private final Drawable mBand;

        private boolean mIsOverlayShown = false;
        private DirectoryFragment.DocumentsAdapter mAdapter;
        private DocumentsAdapter mAdapter;

        RuntimeSelectionEnvironment(RecyclerView rv) {
            mView = rv;
            mAdapter = (DirectoryFragment.DocumentsAdapter) rv.getAdapter();
            mAdapter = (DocumentsAdapter) rv.getAdapter();
            mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
        }

@@ -841,7 +853,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
        }

        @Override
        public String getModelIdFromAdapterPosition(int position) {
        public @Nullable String getModelIdFromAdapterPosition(int position) {
            return mAdapter.getModelId(position);
        }

@@ -964,7 +976,7 @@ public final class MultiSelectManager implements View.OnKeyListener {

        @Override
        public void notifyItemChanged(String id) {
            mAdapter.notifyItemChanged(id, SELECTION_CHANGED_MARKER);
            mAdapter.notifyItemSelectionChanged(id);
        }

        @Override
Loading