Merge "Move Adapters to their own classes."

This commit is contained in:
Steve McKay
2016-01-05 19:29:46 +00:00
committed by Android (Google) Code Review
11 changed files with 939 additions and 306 deletions

View File

@@ -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,10 +903,34 @@ public class DirectoryFragment extends Fragment {
}.execute(selected);
}
private State getDisplayState() {
@Override
public void initDocumentHolder(DocumentHolder holder) {
holder.addClickListener(mItemClickListener);
holder.addOnKeyListener(mSelectionManager);
}
@Override
public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
if (DEBUG_ENABLE_DND) {
setupDragAndDropOnDocumentView(holder.itemView, cursor);
}
}
@Override
public State getDisplayState() {
return ((BaseActivity) getActivity()).getDisplayState();
}
@Override
public Model getModel() {
return mModel;
}
@Override
public boolean isDocumentEnabled(String docMimeType, int docFlags) {
return mTuner.isDocumentEnabled(docMimeType, docFlags);
}
void showEmptyView() {
mEmptyView.setVisibility(View.VISIBLE);
mRecView.setVisibility(View.GONE);
@@ -920,240 +954,6 @@ public class DirectoryFragment extends Fragment {
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.");
}
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));
if (DEBUG_ENABLE_DND) {
setupDragAndDropOnDocumentView(holder.itemView, cursor);
}
}
@Override
public int getItemCount() {
return mModelIds.size();
}
@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);
}
@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);
}
}
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;
}
@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;
}
}
/**
* 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);
}
}
}
private String findCommonMimeType(List<String> mimeTypes) {
String[] commonType = mimeTypes.get(0).split("/");
if (commonType.length != 2) {
@@ -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) {

View File

@@ -0,0 +1,117 @@
/*
* 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);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,223 @@
/*
* 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.Shared.DEBUG;
import static com.android.documentsui.State.MODE_GRID;
import static com.android.documentsui.State.MODE_LIST;
import static com.android.documentsui.State.MODE_UNKNOWN;
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import android.database.Cursor;
import android.provider.DocumentsContract.Document;
import android.support.v7.widget.GridLayoutManager;
import android.util.Log;
import android.util.SparseArray;
import android.view.ViewGroup;
import com.android.documentsui.State;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Adapts from dirlist.Model to something RecyclerView understands.
*/
final class ModelBackedDocumentsAdapter extends DocumentsAdapter {
private static final String TAG = "ModelBackedDocumentsAdapter";
public static final int ITEM_TYPE_DOCUMENT = 1;
public static final int ITEM_TYPE_DIRECTORY = 2;
// Provides access to information needed when creating and view holders. This
// isn't an ideal pattern (more transitive dependency stuff) but good enough for now.
private final Environment mEnv;
private final IconHelper mIconHelper; // a transitive dependency of the holders.
/**
* 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<>();
// List of files that have been deleted. Some transient directory updates
// may happen while files are being deleted. During this time we don't
// want once-hidden files to be re-shown. We only remove
// items from this list when we get a model update where the model
// does not contain a corresponding id. This ensures hidden entries
// don't momentarily re-appear if we get intermediate updates from
// the file system.
private Set<String> mHiddenIds = new HashSet<>();
public ModelBackedDocumentsAdapter(Environment env, IconHelper iconHelper) {
mEnv = env;
mIconHelper = iconHelper;
}
@Override
public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
DocumentHolder holder = null;
final State state = mEnv.getDisplayState();
switch (state.derivedMode) {
case MODE_GRID:
switch (viewType) {
case ITEM_TYPE_DIRECTORY:
holder = new GridDirectoryHolder(mEnv.getContext(), parent);
break;
case ITEM_TYPE_DOCUMENT:
holder = new GridDocumentHolder(mEnv.getContext(), parent, mIconHelper);
break;
default:
throw new IllegalStateException("Unsupported layout type.");
}
break;
case MODE_LIST:
holder = new ListDocumentHolder(mEnv.getContext(), parent, mIconHelper);
break;
case MODE_UNKNOWN:
default:
throw new IllegalStateException("Unsupported layout mode.");
}
mEnv.initDocumentHolder(holder);
return holder;
}
@Override
public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) {
if (payload.contains(SELECTION_CHANGED_MARKER)) {
final boolean selected = mEnv.isSelected(mModelIds.get(position));
holder.setSelected(selected);
} else {
onBindViewHolder(holder, position);
}
}
@Override
public void onBindViewHolder(DocumentHolder holder, int position) {
String modelId = mModelIds.get(position);
Cursor cursor = mEnv.getModel().getItem(modelId);
holder.bind(cursor, modelId, mEnv.getDisplayState());
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
holder.setSelected(mEnv.isSelected(modelId));
holder.setEnabled(mEnv.isDocumentEnabled(docMimeType, docFlags));
mEnv.onBindDocumentHolder(holder, cursor);
}
@Override
public int getItemCount() {
return mModelIds.size();
}
@Override
public void onModelUpdate(Model model) {
if (DEBUG && mHiddenIds.size() > 0) {
Log.d(TAG, "Updating model with hidden ids: " + mHiddenIds);
}
List<String> modelIds = model.getModelIds();
mModelIds = new ArrayList<>(modelIds.size());
for (String id : modelIds) {
if (!mHiddenIds.contains(id)) {
mModelIds.add(id);
} else {
if (DEBUG) Log.d(TAG, "Omitting hidden id from model during update: " + id);
}
}
// Finally remove any hidden ids that aren't present in the model.
// This assumes that model updates represent a complete set of files.
mHiddenIds.retainAll(mModelIds);
}
@Override
public void onModelUpdateFailed(Exception e) {
Log.w(TAG, "Model update failed.", e);
mModelIds.clear();
}
@Override
public String getModelId(int adapterPosition) {
return mModelIds.get(adapterPosition);
}
@Override
public SparseArray<String> hide(String... ids) {
if (DEBUG) Log.d(TAG, "Hiding ids: " + 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)) {
mHiddenIds.add(id);
hiddenItems.put(i, mModelIds.remove(i));
notifyItemRemoved(i);
}
}
return hiddenItems;
}
@Override
public void unhide(SparseArray<String> ids) {
if (DEBUG) Log.d(TAG, "Un-iding ids: " + 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);
mHiddenIds.remove(id);
mModelIds.add(pos, id);
notifyItemInserted(pos);
}
}
@Override
public List<String> getModelIds() {
return mModelIds;
}
@Override
public int getItemViewType(int position) {
return isDirectory(mEnv.getModel(), position)
? ITEM_TYPE_DIRECTORY
: ITEM_TYPE_DOCUMENT;
}
@Override
public void notifyItemSelectionChanged(String id) {
int position = mModelIds.indexOf(id);
if (position >= 0) {
notifyItemChanged(position, SELECTION_CHANGED_MARKER);
} else {
Log.w(TAG, "Item change notification received for unknown item: " + id);
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,211 @@
/*
* 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.internal.util.Preconditions.checkArgument;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.util.SparseArray;
import android.view.ViewGroup;
import java.util.List;
/**
* Adapter wrapper that inserts a sort of line break item between directories and regular files.
* Only needs to be used in GRID mode...at this time.
*/
final class SectionBreakDocumentsAdapterWrapper extends DocumentsAdapter {
private static final String TAG = "SectionBreakDocumentsAdapterWrapper";
private static final int ITEM_TYPE_SECTION_BREAK = Integer.MAX_VALUE;
private final Environment mEnv;
private final DocumentsAdapter mDelegate;
private int mBreakPosition = -1;
SectionBreakDocumentsAdapterWrapper(Environment environment, DocumentsAdapter delegate) {
mEnv = environment;
mDelegate = delegate;
// Events and information flows two ways between recycler view and adapter.
// So we need to listen to events on our delegate and forward them
// to our listeners with a corrected position.
AdapterDataObserver adapterDataObserver = new AdapterDataObserver() {
public void onChanged() {
throw new UnsupportedOperationException();
}
public void onItemRangeChanged(int positionStart, int itemCount) {
checkArgument(itemCount == 1);
}
public void onItemRangeInserted(int positionStart, int itemCount) {
checkArgument(itemCount == 1);
if (positionStart < mBreakPosition) {
mBreakPosition++;
}
notifyItemRangeInserted(toViewPosition(positionStart), itemCount);
}
public void onItemRangeRemoved(int positionStart, int itemCount) {
checkArgument(itemCount == 1);
if (positionStart < mBreakPosition) {
mBreakPosition--;
}
notifyItemRangeRemoved(toViewPosition(positionStart), itemCount);
}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
throw new UnsupportedOperationException();
}
};
mDelegate.registerAdapterDataObserver(adapterDataObserver);
}
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_SECTION_BREAK) {
return mEnv.getColumnCount();
} else {
return 1;
}
}
};
}
@Override
public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_SECTION_BREAK) {
return new EmptyDocumentHolder(mEnv.getContext());
} else {
return mDelegate.createViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) {
if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload);
}
}
@Override
public void onBindViewHolder(DocumentHolder holder, int p) {
if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
mDelegate.onBindViewHolder(holder, toDelegatePosition(p));
}
}
@Override
public int getItemCount() {
return mBreakPosition == -1
? mDelegate.getItemCount()
: mDelegate.getItemCount() + 1;
}
@Override
public void onModelUpdate(Model model) {
mDelegate.onModelUpdate(model);
mBreakPosition = -1;
// 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.
List<String> modelIds = mDelegate.getModelIds();
for (int i = 0; i < modelIds.size(); i++) {
if (!isDirectory(model, i)) {
mBreakPosition = i;
break;
}
}
}
@Override
public void onModelUpdateFailed(Exception e) {
mDelegate.onModelUpdateFailed(e);
}
@Override
public int getItemViewType(int p) {
if (p == mBreakPosition) {
return ITEM_TYPE_SECTION_BREAK;
} else {
return mDelegate.getItemViewType(toDelegatePosition(p));
}
}
/**
* Returns the position of an item in the delegate, adjusting
* values that are greater than the break position.
*
* @param p Position within the view
* @return Position within the delegate
*/
private int toDelegatePosition(int p) {
return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 : p;
}
/**
* Returns the position of an item in the view, adjusting
* values that are greater than the break position.
*
* @param p Position within the delegate
* @return Position within the view
*/
private int toViewPosition(int p) {
// If position is greater than or equal to the break, increase by one.
return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p;
}
@Override
public SparseArray<String> hide(String... ids) {
// NOTE: We hear about these changes and adjust break position
// in our AdapterDataObserver.
return mDelegate.hide(ids);
}
@Override
void unhide(SparseArray<String> ids) {
// NOTE: We hear about these changes and adjust break position
// in our AdapterDataObserver.
mDelegate.unhide(ids);
}
@Override
List<String> getModelIds() {
return mDelegate.getModelIds();
}
@Override
String getModelId(int p) {
return (p == mBreakPosition) ? null : mDelegate.getModelId(toDelegatePosition(p));
}
@Override
public void notifyItemSelectionChanged(String id) {
mDelegate.notifyItemSelectionChanged(id);
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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 android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.ViewGroup;
import com.android.documentsui.State;
import java.util.List;
@SmallTest
public class ModelBackedDocumentsAdapterTest extends AndroidTestCase {
private static final String AUTHORITY = "test_authority";
private static final String[] NAMES = new String[] {
"4",
"foo",
"1",
"bar",
"*(Ljifl;a",
"0",
"baz",
"2",
"3",
"%$%VD"
};
private TestModel model;
private ModelBackedDocumentsAdapter adapter;
public void setUp() {
final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
model = new TestModel(testContext, AUTHORITY);
model.update(NAMES);
DocumentsAdapter.Environment env = new TestEnvironment(testContext);
adapter = new ModelBackedDocumentsAdapter(
env, new IconHelper(testContext, State.MODE_GRID));
adapter.onModelUpdate(model);
}
// Tests that the item count is correct.
public void testItemCount() {
assertEquals(model.getItemCount(), adapter.getItemCount());
}
// Tests that the item count is correct.
public void testHide_ItemCount() {
List<String> ids = model.getModelIds();
adapter.hide(ids.get(0), ids.get(1));
assertEquals(model.getItemCount() - 2, adapter.getItemCount());
}
private final class TestEnvironment implements DocumentsAdapter.Environment {
private final Context testContext;
private TestEnvironment(Context testContext) {
this.testContext = testContext;
}
@Override
public boolean isSelected(String id) {
return false;
}
@Override
public boolean isDocumentEnabled(String mimeType, int flags) {
return true;
}
@Override
public void initDocumentHolder(DocumentHolder holder) {}
@Override
public Model getModel() {
return model;
}
@Override
public State getDisplayState() {
return null;
}
@Override
public Context getContext() {
return testContext;
}
@Override
public int getColumnCount() {
return 4;
}
@Override
public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {}
}
private static class DummyListener implements Model.UpdateListener {
public void onModelUpdate(Model model) {}
public void onModelUpdateFailed(Exception e) {}
}
private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public int getItemCount() { return 0; }
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return null;
}
}
}

View File

@@ -21,16 +21,10 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.ViewGroup;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.RootCursorWrapper;
@@ -49,6 +43,7 @@ public class ModelTest extends AndroidTestCase {
private static final int ITEM_COUNT = 10;
private static final String AUTHORITY = "test_authority";
private static final String[] COLUMNS = new String[]{
RootCursorWrapper.COLUMN_AUTHORITY,
Document.COLUMN_DOCUMENT_ID,
@@ -57,23 +52,24 @@ public class ModelTest extends AndroidTestCase {
Document.COLUMN_SIZE,
Document.COLUMN_MIME_TYPE
};
private static Cursor cursor;
private static final String[] NAMES = new String[] {
"4",
"foo",
"1",
"bar",
"*(Ljifl;a",
"0",
"baz",
"2",
"3",
"%$%VD"
};
private Cursor cursor;
private Context context;
private Model model;
private TestContentProvider provider;
private static final String[] NAMES = new String[] {
"4",
"foo",
"1",
"bar",
"*(Ljifl;a",
"0",
"baz",
"2",
"3",
"%$%VD"
};
public void setUp() {
setupTestContext();
@@ -97,7 +93,7 @@ public class ModelTest extends AndroidTestCase {
r.cursor = cursor;
// Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
model = new Model(context, new DummyAdapter());
model = new Model(context);
model.addUpdateListener(new DummyListener());
model.update(r);
}
@@ -326,32 +322,4 @@ public class ModelTest extends AndroidTestCase {
public void onModelUpdate(Model model) {}
public void onModelUpdateFailed(Exception e) {}
}
private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public int getItemCount() { return 0; }
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return null;
}
}
private static class TestContentProvider extends MockContentProvider {
List<Uri> mDeleted = new ArrayList<>();
@Override
public Bundle call(String method, String arg, Bundle extras) {
// Intercept and log delete method calls.
if (DocumentsContract.METHOD_DELETE_DOCUMENT.equals(method)) {
final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
mDeleted.add(documentUri);
return new Bundle();
} else {
return super.call(method, arg, extras);
}
}
public void assertWasDeleted(DocumentInfo doc) {
assertTrue(mDeleted.contains(doc.derivedUri));
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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 android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.test.mock.MockContentProvider;
import com.android.documentsui.model.DocumentInfo;
import java.util.ArrayList;
import java.util.List;
/**
* A very simple test double for ContentProvider. Useful in this package only.
*/
class TestContentProvider extends MockContentProvider {
List<Uri> mDeleted = new ArrayList<>();
@Override
public Bundle call(String method, String arg, Bundle extras) {
// Intercept and log delete method calls.
if (DocumentsContract.METHOD_DELETE_DOCUMENT.equals(method)) {
final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
mDeleted.add(documentUri);
return new Bundle();
} else {
return super.call(method, arg, extras);
}
}
public void assertWasDeleted(DocumentInfo doc) {
ModelTest.assertTrue(mDeleted.contains(doc.derivedUri));
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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 android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.test.mock.MockContentResolver;
public final class TestContext {
/**
* Returns a Context configured with test provider for authority.
*/
static Context createStorageTestContext(Context context, String authority) {
final MockContentResolver testResolver = new MockContentResolver();
TestContentProvider provider = new TestContentProvider();
testResolver.addProvider(authority, provider);
return new ContextWrapper(context) {
@Override
public ContentResolver getContentResolver() {
return testResolver;
}
};
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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 android.content.Context;
import android.database.MatrixCursor;
import android.provider.DocumentsContract.Document;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.RootCursorWrapper;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import java.util.Random;
import java.util.Set;
public class TestModel extends Model {
private static final String[] COLUMNS = new String[]{
RootCursorWrapper.COLUMN_AUTHORITY,
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_FLAGS,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_SIZE,
Document.COLUMN_MIME_TYPE
};
private final String mAuthority;
private Set<String> mDeleted;
/**
* Creates a new context. context must be configured with provider for authority.
* @see TestContext#createStorageTestContext(Context, String).
*/
public TestModel(Context context, String authority) {
super(context);
mAuthority = authority;
}
void update(String... names) {
Random rand = new Random();
MatrixCursor c = new MatrixCursor(COLUMNS);
for (int i = 0; i < names.length; i++) {
MatrixCursor.RowBuilder row = c.newRow();
row.add(RootCursorWrapper.COLUMN_AUTHORITY, mAuthority);
row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
// Generate random document names and sizes. This forces the model's internal sort code
// to actually do something.
row.add(Document.COLUMN_DISPLAY_NAME, names[i]);
row.add(Document.COLUMN_SIZE, rand.nextInt());
}
DirectoryResult r = new DirectoryResult();
r.cursor = c;
update(r);
}
@Override
public void delete(Selection selected, DeletionListener listener) {
for (String id : selected.getAll()) {
mDeleted.add(id);
}
listener.onCompletion();
}
}