Don't proxy calls to DocumentsAdapter in MultiSelManager.

We were bouncing calls out to the adapter pulled from RecyclerView,
but the adapter is easy to write a test double for and
we can readily inject the adapter when needed at runtime.

Eliminates unnecessary indirection.
Also, protect against failure when documents can't be loaded for delete.

Change-Id: Ief6585bf2e3e4fd407d801d485a9d7cd888b8500
This commit is contained in:
Steve McKay
2016-01-05 15:27:17 -08:00
parent ab6fb6fcaf
commit 44408260ba
8 changed files with 157 additions and 135 deletions

View File

@@ -332,6 +332,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
// into the selection manager.
mSelectionManager = new MultiSelectManager(
mRecView,
mAdapter,
state.allowMultiple
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE);

View File

@@ -414,8 +414,14 @@ public class Model implements SiblingProvider {
@Override
protected Void doInBackground(Selection... selected) {
List<DocumentInfo> toDelete = getDocuments(selected[0]);
mHadTrouble = false;
List<DocumentInfo> toDelete = null;
try {
toDelete = getDocuments(selected[0]);
} catch (NullPointerException e) {
Log.w(TAG, "Failed to retrieve documents for delete.");
mHadTrouble = true;
return null;
}
for (DocumentInfo doc : toDelete) {
if (!doc.isDeleteSupported()) {

View File

@@ -68,21 +68,23 @@ public final class MultiSelectManager implements View.OnKeyListener {
private final Selection mSelection = new Selection();
private Range mRanger;
private SelectionEnvironment mEnvironment;
private final SelectionEnvironment mEnvironment;
private final DocumentsAdapter mAdapter;
private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
private Range mRanger;
private boolean mSingleSelect;
@Nullable private BandController mBandManager;
/**
* @param recyclerView
* @param mode Selection mode
*/
public MultiSelectManager(final RecyclerView recyclerView, int mode) {
this(new RuntimeSelectionEnvironment(recyclerView), mode);
public MultiSelectManager(
final RecyclerView recyclerView, DocumentsAdapter adapter, int mode) {
this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode);
if (mode == MODE_MULTIPLE) {
mBandManager = new BandController();
@@ -133,11 +135,12 @@ public final class MultiSelectManager implements View.OnKeyListener {
* @hide
*/
@VisibleForTesting
MultiSelectManager(SelectionEnvironment environment, int mode) {
MultiSelectManager(SelectionEnvironment environment, DocumentsAdapter adapter, int mode) {
mEnvironment = checkNotNull(environment, "'environment' cannot be null.");
mAdapter = checkNotNull(adapter, "'adapter' cannot be null.");
mSingleSelect = mode == MODE_SINGLE;
mEnvironment.registerDataObserver(
mAdapter.registerAdapterDataObserver(
new RecyclerView.AdapterDataObserver() {
private List<String> mModelIds;
@@ -146,7 +149,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
public void onChanged() {
// TODO: This is causing b/22765812
mSelection.clear();
mModelIds = mEnvironment.getModelIds();
mModelIds = mAdapter.getModelIds();
}
@Override
@@ -336,7 +339,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
return;
}
String id = mEnvironment.getModelIdFromAdapterPosition(position);
String id = mAdapter.getModelId(position);
if (id != null) {
toggleSelection(id);
}
@@ -388,7 +391,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
return;
}
if (mSelection.contains(mEnvironment.getModelIdFromAdapterPosition(position))) {
if (mSelection.contains(mAdapter.getModelId(position))) {
mRanger = new Range(position);
}
}
@@ -405,7 +408,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
private void updateRange(int begin, int end, boolean selected) {
checkState(end >= begin);
for (int i = begin; i <= end; i++) {
String id = mEnvironment.getModelIdFromAdapterPosition(i);
String id = mAdapter.getModelId(i);
if (id == null) {
continue;
}
@@ -473,7 +476,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
for (int i = lastListener; i > -1; i--) {
mCallbacks.get(i).onItemStateChanged(id, selected);
}
mEnvironment.notifyItemChanged(id);
mAdapter.onItemSelectionChanged(id);
}
/**
@@ -811,16 +814,6 @@ public final class MultiSelectManager implements View.OnKeyListener {
int getChildCount();
int getVisibleChildCount();
void focusItem(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);
void registerDataObserver(RecyclerView.AdapterDataObserver observer);
}
/** Recycler view facade implementation backed by good ol' RecyclerView. */
@@ -830,11 +823,9 @@ public final class MultiSelectManager implements View.OnKeyListener {
private final Drawable mBand;
private boolean mIsOverlayShown = false;
private DocumentsAdapter mAdapter;
RuntimeSelectionEnvironment(RecyclerView rv) {
mView = rv;
mAdapter = (DocumentsAdapter) rv.getAdapter();
RuntimeSelectionEnvironment(RecyclerView view) {
mView = view;
mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
}
@@ -852,11 +843,6 @@ public final class MultiSelectManager implements View.OnKeyListener {
return getAdapterPositionForChildView(mView.getChildAt(index));
}
@Override
public @Nullable String getModelIdFromAdapterPosition(int position) {
return mAdapter.getModelId(position);
}
@Override
public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
mView.addOnScrollListener(listener);
@@ -973,26 +959,6 @@ public final class MultiSelectManager implements View.OnKeyListener {
});
}
}
@Override
public void notifyItemChanged(String id) {
mAdapter.onItemSelectionChanged(id);
}
@Override
public int getItemCount() {
return mAdapter.getItemCount();
}
@Override
public void registerDataObserver(RecyclerView.AdapterDataObserver observer) {
mAdapter.registerAdapterDataObserver(observer);
}
@Override
public List<String> getModelIds() {
return mAdapter.getModelIds();
}
}
public interface Callback {
@@ -1049,7 +1015,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
mModelBuilder = new Runnable() {
@Override
public void run() {
mModel = new GridModel(mEnvironment);
mModel = new GridModel(mEnvironment, mAdapter);
mModel.addOnSelectionChangedListener(BandController.this);
}
};
@@ -1100,7 +1066,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
return !isActive()
&& e.isMouseEvent() // a mouse
&& e.isActionDown() // the initial button press
&& mEnvironment.getItemCount() > 0
&& mAdapter.getItemCount() > 0
&& e.getItemPosition() == RecyclerView.NO_ID; // in empty space
}
@@ -1177,7 +1143,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
mModel.endSelection();
int firstSelected = mModel.getPositionNearestOrigin();
if (firstSelected != NOT_SET) {
if (mSelection.contains(mEnvironment.getModelIdFromAdapterPosition(firstSelected))) {
if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
// TODO: firstSelected should really be lastSelected, we want to anchor the item
// where the mouse-up occurred.
setSelectionRangeBegin(firstSelected);
@@ -1339,6 +1305,8 @@ public final class MultiSelectManager implements View.OnKeyListener {
private static final int LOWER_RIGHT = LOWER | RIGHT;
private final SelectionEnvironment mHelper;
private final DocumentsAdapter mAdapter;
private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
new ArrayList<>();
@@ -1376,8 +1344,9 @@ public final class MultiSelectManager implements View.OnKeyListener {
// should expand from when Shift+click is used.
private int mPositionNearestOrigin = NOT_SET;
GridModel(SelectionEnvironment helper) {
GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
mHelper = helper;
mAdapter = adapter;
mHelper.addOnScrollListener(this);
}
@@ -1580,7 +1549,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
// position. Use a sentry value to prevent erroneously selecting item 0.
int position = items.get(items.keyAt(row), NOT_SET);
if (position != NOT_SET) {
String id = mHelper.getModelIdFromAdapterPosition(position);
String id = mAdapter.getModelId(position);
if (id != null) {
// The adapter inserts items for UI layout purposes that aren't associated
// with files. Those will have a null model ID. Don't select them.
@@ -1989,7 +1958,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
position = 0;
} else if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
position = mEnvironment.getItemCount() - 1;
position = mAdapter.getItemCount() - 1;
} else {
// Find a navigation target based on the arrow key that the user pressed. Ignore
// navigation targets that aren't items in the recycler view.

View File

@@ -84,7 +84,8 @@ public class ModelBackedDocumentsAdapterTest extends AndroidTestCase {
// Tests that the items can be hidden and unhidden.
public void testUnhide_PreservesOrder() {
List<String> ids = mModel.getModelIds();
SparseArray<String> hidden = mAdapter.hide(ids.toArray(new String[ids.size()]));
SparseArray<String> hidden = mAdapter.hide(
ids.get(0), ids.get(1), ids.get(5), ids.get(9));
mAdapter.unhide(hidden);
// Finally ensure the restored items are in the original order

View File

@@ -23,6 +23,7 @@ import android.util.SparseBooleanArray;
import com.android.documentsui.TestInputEvent;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.google.common.collect.Lists;
import java.util.ArrayList;
@@ -44,11 +45,13 @@ public class MultiSelectManagerTest extends AndroidTestCase {
private MultiSelectManager mManager;
private TestCallback mCallback;
private TestSelectionEnvironment mEnv;
private TestDocumentsAdapter mAdapter;
public void setUp() throws Exception {
mCallback = new TestCallback();
mEnv = new TestSelectionEnvironment(items);
mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_MULTIPLE);
mAdapter = new TestDocumentsAdapter(items);
mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE);
mManager.addCallback(mCallback);
}
@@ -164,7 +167,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
}
public void testSingleSelectMode() {
mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE);
mManager.addCallback(mCallback);
longPress(20);
tap(13);
@@ -172,7 +175,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
}
public void testSingleSelectMode_ShiftTap() {
mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE);
mManager.addCallback(mCallback);
longPress(13);
shiftTap(20);
@@ -180,7 +183,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
}
public void testSingleSelectMode_ShiftDoesNotExtendSelection() {
mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE);
mManager.addCallback(mCallback);
longPress(20);
keyToPosition(22, true);

View File

@@ -20,7 +20,6 @@ import static com.android.documentsui.dirlist.MultiSelectManager.GridModel.NOT_S
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
@@ -28,7 +27,7 @@ import android.view.View;
import com.android.documentsui.dirlist.MultiSelectManager.GridModel;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
@SmallTest
@@ -38,15 +37,28 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
private static final int CHILD_VIEW_EDGE_PX = 100;
private static final int VIEWPORT_HEIGHT = 500;
private static GridModel model;
private static TestEnvironment env;
private static Set<String> lastSelection;
private static int viewWidth;
private GridModel model;
private TestEnvironment env;
private TestDocumentsAdapter adapter;
private Set<String> lastSelection;
private int viewWidth;
private static void setUp(int numChildren, int numColumns) {
private void initData(final int numChildren, int numColumns) {
env = new TestEnvironment(numChildren, numColumns);
adapter = new TestDocumentsAdapter(new ArrayList<String>()) {
@Override
public String getModelId(int position) {
return Integer.toString(position);
}
@Override
public int getItemCount() {
return numChildren;
}
};
viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
model = new GridModel(env);
model = new GridModel(env, adapter);
model.addOnSelectionChangedListener(
new GridModel.OnSelectionChangedListener() {
@Override
@@ -64,7 +76,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testSelectionLeftOfItems() {
setUp(20, 5);
initData(20, 5);
model.startSelection(new Point(0, 10));
model.resizeSelection(new Point(1, 11));
assertSelected();
@@ -72,7 +84,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testSelectionRightOfItems() {
setUp(20, 4);
initData(20, 4);
model.startSelection(new Point(viewWidth - 1, 10));
model.resizeSelection(new Point(viewWidth - 2, 11));
assertSelected();
@@ -80,7 +92,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testSelectionAboveItems() {
setUp(20, 4);
initData(20, 4);
model.startSelection(new Point(10, 0));
model.resizeSelection(new Point(11, 1));
assertSelected();
@@ -88,7 +100,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testSelectionBelowItems() {
setUp(5, 4);
initData(5, 4);
model.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
model.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
assertSelected();
@@ -96,7 +108,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testVerticalSelectionBetweenItems() {
setUp(20, 4);
initData(20, 4);
model.startSelection(new Point(106, 0));
model.resizeSelection(new Point(107, 200));
assertSelected();
@@ -104,7 +116,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testHorizontalSelectionBetweenItems() {
setUp(20, 4);
initData(20, 4);
model.startSelection(new Point(0, 105));
model.resizeSelection(new Point(200, 106));
assertSelected();
@@ -112,7 +124,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testGrowingAndShrinkingSelection() {
setUp(20, 4);
initData(20, 4);
model.startSelection(new Point(0, 0));
model.resizeSelection(new Point(5, 5));
assertSelected(0);
@@ -142,7 +154,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testSelectionMovingAroundOrigin() {
setUp(16, 4);
initData(16, 4);
model.startSelection(new Point(210, 210));
model.resizeSelection(new Point(viewWidth - 1, 0));
assertSelected(2, 3, 6, 7);
@@ -156,7 +168,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
}
public void testScrollingBandSelect() {
setUp(40, 4);
initData(40, 4);
model.startSelection(new Point(0, 0));
model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
assertSelected(0, 4, 8, 12, 16);
@@ -173,14 +185,14 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
assertEquals(0, model.getPositionNearestOrigin());
}
private static void assertSelected(int... selectedPositions) {
private void assertSelected(int... selectedPositions) {
assertEquals(selectedPositions.length, lastSelection.size());
for (int position : selectedPositions) {
assertTrue(lastSelection.contains(Integer.toString(position)));
}
}
private static void scroll(int dy) {
private void scroll(int dy) {
assertTrue(env.verticalOffset + VIEWPORT_HEIGHT + dy <= env.getTotalHeight());
env.verticalOffset += dy;
model.onScrolled(null, 0, dy);
@@ -322,30 +334,5 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
public void focusItem(int i) {
throw new UnsupportedOperationException();
}
@Override
public String getModelIdFromAdapterPosition(int position) {
return Integer.toString(position);
}
@Override
public int getItemCount() {
return mNumChildren;
}
@Override
public List<String> getModelIds() {
return null;
}
@Override
public void notifyItemChanged(String id) {
throw new UnsupportedOperationException();
}
@Override
public void registerDataObserver(AdapterDataObserver observer) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.util.SparseArray;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* A skeletal {@link DocumentsAdapter} test double.
*/
public class TestDocumentsAdapter extends DocumentsAdapter {
List<String> mModelIds = new ArrayList<>();
public TestDocumentsAdapter(List<String> modelIds) {
mModelIds = modelIds;
}
@Override
public void onModelUpdate(Model model) {
}
@Override
public void onModelUpdateFailed(Exception e) {
}
@Override
List<String> getModelIds() {
return mModelIds;
}
@Override
void onItemSelectionChanged(String id) {
}
@Override
String getModelId(int position) {
return mModelIds.get(position);
}
@Override
public SparseArray<String> hide(String... ids) {
throw new UnsupportedOperationException();
}
@Override
void unhide(SparseArray<String> ids) {
throw new UnsupportedOperationException();
}
@Override
public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
throw new UnsupportedOperationException();
}
@Override
public void onBindViewHolder(DocumentHolder holder, int position) {
throw new UnsupportedOperationException();
}
@Override
public int getItemCount() {
return mModelIds.size();
}
}

View File

@@ -18,7 +18,6 @@ package com.android.documentsui.dirlist;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
@@ -28,10 +27,7 @@ import java.util.List;
public class TestSelectionEnvironment implements SelectionEnvironment {
private List<String> mItems;
public TestSelectionEnvironment(List<String> items) {
mItems = items;
}
@Override
@@ -114,27 +110,4 @@ public class TestSelectionEnvironment implements SelectionEnvironment {
@Override
public void focusItem(int position) {
}
@Override
public String getModelIdFromAdapterPosition(int position) {
return mItems.get(position);
}
@Override
public int getItemCount() {
return mItems.size();
}
@Override
public List<String> getModelIds() {
return null;
}
@Override
public void notifyItemChanged(String id) {
}
@Override
public void registerDataObserver(AdapterDataObserver observer) {
}
}