Loading src/com/android/documentsui/dirlist/DirectoryFragment.java +47 −246 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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"; Loading Loading @@ -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 = Loading Loading @@ -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); Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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) { Loading Loading @@ -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); } Loading @@ -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) { Loading src/com/android/documentsui/dirlist/DocumentsAdapter.java 0 → 100644 +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); } } src/com/android/documentsui/dirlist/Model.java +1 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java 0 → 100644 +223 −0 File added.Preview size limit exceeded, changes collapsed. Show changes src/com/android/documentsui/dirlist/MultiSelectManager.java +22 −10 Original line number Diff line number Diff line Loading @@ -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; /** Loading Loading @@ -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); } } /** Loading @@ -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); Loading Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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); Loading Loading @@ -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); } Loading Loading @@ -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); Loading @@ -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); } Loading @@ -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); } Loading Loading @@ -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 Loading
src/com/android/documentsui/dirlist/DirectoryFragment.java +47 −246 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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"; Loading Loading @@ -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 = Loading Loading @@ -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); Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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) { Loading Loading @@ -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); } Loading @@ -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) { Loading
src/com/android/documentsui/dirlist/DocumentsAdapter.java 0 → 100644 +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); } }
src/com/android/documentsui/dirlist/Model.java +1 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } Loading
src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java 0 → 100644 +223 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
src/com/android/documentsui/dirlist/MultiSelectManager.java +22 −10 Original line number Diff line number Diff line Loading @@ -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; /** Loading Loading @@ -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); } } /** Loading @@ -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); Loading Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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); Loading Loading @@ -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); } Loading Loading @@ -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); Loading @@ -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); } Loading @@ -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); } Loading Loading @@ -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