Merge "Move Adapters to their own classes."
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user